Compare commits

...

18 Commits

Author SHA1 Message Date
67a967da67 ArgoCD-Documentation updated 2026-02-20 13:52:41 +01:00
6af0b02442 Documentation for data import updated 2026-02-20 10:03:52 +01:00
f7a20648b2 Changes in referenz entry field
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 31s
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
2026-02-12 15:40:21 +01:00
bae8c71028 Deploy last commit
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 36s
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
2026-02-12 15:09:00 +01:00
a0495fdea0 Einleitung and Geltungsbereich now draggable 2026-02-12 15:05:52 +01:00
c125238f12 NFS adapted to use share "Kubernetesdata" 2026-02-10 16:04:53 +01:00
f5c0d9beac Delete .gitea/workflows/check_code_in_sonarqube.yaml 2026-02-10 07:37:38 +00:00
70752a2482 Deployment
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 2m30s
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 17s
SonarQube Scan / SonarQube Trigger (push) Failing after 15s
2026-02-09 08:39:03 +01:00
633ecabdb9 Admin interface for Referenzen fixed
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
2026-02-09 08:37:47 +01:00
27e78d6f39 New images in Dockerfile
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 6s
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 6s
SonarQube Scan / SonarQube Trigger (push) Failing after 9s
2026-02-05 12:41:29 +01:00
ad9d55e986 Version tag now pulled from configmap
Some checks failed
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 8s
SonarQube Scan / SonarQube Trigger (push) Failing after 12s
2026-01-27 13:11:51 +01:00
996ea628c7 Another attempt at serving media files 2026-01-27 13:03:54 +01:00
523b991493 Media files (diagrams) should now be served regardless of debug mode.
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
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 7s
2026-01-27 12:58:07 +01:00
310c4fdd0b Changed user-id to 99 to comply with NFS-mount
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Failing after 10s
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 8s
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
2026-01-27 11:35:08 +01:00
353a8a5697 Bug in Referenzen fixed
Some checks failed
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 16s
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
SonarQube Scan / SonarQube Trigger (push) Failing after 5s
2026-01-20 10:36:16 +01:00
f1d3c88a45 Merge pull request 'upgrade/django6' (#17) from upgrade/django6 into development
Some checks failed
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
SonarQube Scan / SonarQube Trigger (push) Failing after 5s
Reviewed-on: #17
2026-01-20 09:21:43 +00:00
996584ef68 Some changes after code review; Deploying to Development
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
Build containers when image tags change / build-if-image-changed (., web, containers, main container, git.baumann.gr/adebaumann/vui) (push) Successful in 5s
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
SonarQube Scan / SonarQube Trigger (pull_request) Failing after 10s
2026-01-20 10:17:29 +01:00
18fac6e8b9 Code reviewed; Package versions updated to latest (incl. Django 6)
Some checks failed
SonarQube Scan / SonarQube Trigger (push) Has been cancelled
2026-01-20 09:51:08 +01:00
45 changed files with 911 additions and 807 deletions

View File

@@ -1,67 +0,0 @@
on:
push:
# branches:
# - main
# - development
pull_request:
types: [opened, synchronize, reopened]
name: SonarQube Scan
jobs:
sonarqube:
name: SonarQube Trigger
runs-on: ubuntu-latest
steps:
- name: Checking out
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run tests with coverage
run: |
coverage run --source='.' manage.py test
coverage xml
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Cache SonarQube packages
uses: actions/cache@v3
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Download and setup SonarScanner
run: |
mkdir -p $HOME/.sonar
wget -q https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-5.0.1.3006-linux.zip
unzip -q sonar-scanner-cli-5.0.1.3006-linux.zip -d $HOME/.sonar/
echo "$HOME/.sonar/sonar-scanner-5.0.1.3006-linux/bin" >> $GITHUB_PATH
- name: Verify Java version
run: java -version
- name: SonarQube Scan
env:
SONAR_HOST_URL: ${{ secrets.SONARQUBE_HOST }}
SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }}
run: |
sonar-scanner \
-Dsonar.projectKey=${{ github.event.repository.name }} \
-Dsonar.sources=. \
-Dsonar.host.url=${SONAR_HOST_URL} \
-Dsonar.token=${SONAR_TOKEN} \
-Dsonar.python.coverage.reportPaths=coverage.xml

3
.gitignore vendored
View File

@@ -16,4 +16,5 @@ AGENT*.md
# Diagram cache directory # Diagram cache directory
media/diagram_cache/ media/diagram_cache/
.env .env
data/db.sqlite3 data/
dataremote/

View File

@@ -1,14 +1,18 @@
FROM python:3.15-rc-trixie AS baustelle FROM python:3.15.0a5-trixie AS baustelle
RUN mkdir /app RUN mkdir /app
WORKDIR /app WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
RUN pip install --upgrade pip RUN pip install --upgrade pip
COPY requirements.txt /app/ COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt && \
apt-get update && \
apt-get install -y curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
FROM python:3.15-rc-slim-trixie FROM python:3.15.0a5-slim-trixie
RUN useradd -m -r appuser && \ RUN useradd -m -r -u 99 appuser && \
mkdir /app && \ mkdir /app && \
chown -R appuser /app chown -R appuser /app
@@ -35,5 +39,8 @@ RUN rm -rvf /app/Dockerfile* \
/app/scripts \ /app/scripts \
/app/test_*.py && \ /app/test_*.py && \
python3 /app/manage.py collectstatic --noinput python3 /app/manage.py collectstatic --noinput
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/ || exit 1
CMD ["gunicorn","--bind","0.0.0.0:8000","--workers","3","VorgabenUI.wsgi:application"] CMD ["gunicorn","--bind","0.0.0.0:8000","--workers","3","VorgabenUI.wsgi:application"]

View File

@@ -13,21 +13,29 @@ This directory contains the ArgoCD application manifests for deploying the Vorga
- **Storage Class**: Uses NFS storage class for shared storage across multiple pods - **Storage Class**: Uses NFS storage class for shared storage across multiple pods
- **Namespace**: vorgabenui - **Namespace**: vorgabenui
#### `configmap.yaml`
- **Purpose**: Django application configuration
- **Contains**: Environment variables, application settings, version information
- **Namespace**: vorgabenui
- **Version**: 0.990
#### `deployment.yaml` #### `deployment.yaml`
- **Purpose**: Main application deployment configuration - **Purpose**: Main application deployment configuration
- **Contains**: Django application container, environment variables, resource limits - **Contains**: Django application container, environment variables, resource limits
- **Replicas**: Configurable replica count for high availability - **Replicas**: Configurable replica count for high availability
- **Version**: 0.990
#### `ingress.yaml` #### `ingress.yaml`
- **Purpose**: External access configuration - **Purpose**: External access configuration
- **Host**: Configurable hostname for the application - **Host**: Configurable hostname for the application
- **TLS**: SSL/TLS termination configuration - **TLS**: SSL/TLS termination configuration
- **Backend**: Routes traffic to the Django application service - **Backend**: Routes traffic to the Django application service
- **Ingress Class**: traefik
#### `nfs-pv.yaml` #### `nfs-pv.yaml`
- **Purpose**: PersistentVolume definition for NFS storage - **Purpose**: PersistentVolume definition for NFS storage
- **Server**: 192.168.17.199 - **Server**: 192.168.17.199
- **Path**: /mnt/user/vorgabenui - **Path**: /mnt/user/kubernetesdata/vorgabenui
- **Access**: ReadWriteMany for multi-pod access - **Access**: ReadWriteMany for multi-pod access
- **Reclaim Policy**: Retain (data preserved after PVC deletion) - **Reclaim Policy**: Retain (data preserved after PVC deletion)
@@ -40,14 +48,21 @@ This directory contains the ArgoCD application manifests for deploying the Vorga
#### `diagrammer.yaml` #### `diagrammer.yaml`
- **Purpose**: Deployment configuration for the diagram generation service - **Purpose**: Deployment configuration for the diagram generation service
- **Function**: Handles diagram creation and caching for the application - **Function**: Handles diagram creation and caching for the application
- **Version**: 0.026
## NFS Storage Configuration #### `secret.yaml` (Template)
- **Purpose**: Template for Django SECRET_KEY secret
- **Contains**: Secret key configuration for cryptographic operations
- **Namespace**: vorgabenui
- **Generated by**: `deploy-argocd-secret.sh` script
- **Version**: 0.026
### Prerequisites #### `secret.yaml` (Template)
1. NFS server must be running at 192.168.17.199 - **Purpose**: Template for Django SECRET_KEY secret
2. The directory `/mnt/user/vorgabenui` must exist and be exported - **Contains**: Secret key configuration for cryptographic operations
3. Kubernetes nodes must have NFS client utilities installed - **Namespace**: vorgabenui
4. For MicroK8s: `microk8s enable nfs` - **Generated by**: `deploy-argocd-secret.sh` script
- **Version**: 0.026
## MicroK8s Addons Required ## MicroK8s Addons Required
@@ -136,7 +151,7 @@ microk8s kubectl get pods -n ingress
microk8s kubectl get svc -n ingress microk8s kubectl get svc -n ingress
# Test ingress connectivity # Test ingress connectivity
curl -k https://your-domain.com curl -k https://vorgabenportal.knowyoursecurity.com
``` ```
#### Storage Issues #### Storage Issues
@@ -159,24 +174,143 @@ On the NFS server (192.168.17.199), ensure the following:
```bash ```bash
# Create the shared directory # Create the shared directory
sudo mkdir -p /mnt/user/vorgabenui sudo mkdir -p /mnt/user/kubernetesdata/vorgabenui
sudo chmod 755 /mnt/user/vorgabenui sudo chmod 755 /mnt/user/kubernetesdata/vorgabenui
# Add to /etc/exports # Add to /etc/exports
echo "/mnt/user/vorgabenui *(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exports echo "/mnt/user/kubernetesdata/vorgabenui *(rw,sync,no_subtree_check,no_root_squash)" | sudo tee -a /etc/exports
# Export the directory # Export the directory
sudo exportfs -a sudo exportfs -a
sudo systemctl restart nfs-kernel-server sudo systemctl restart nfs-kernel-server
``` ```
## Configuration Management
### ConfigMap Deployment
The Django application uses a ConfigMap for application configuration. The ConfigMap contains environment variables and settings for the Django application.
#### ConfigMap File
- **File**: `configmap.yaml`
- **Name**: `django-config`
- **Namespace**: `vorgabenui`
- **Version**: 0.990
#### Configuration Contents
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: django-config
namespace: vorgabenui
data:
# Django Configuration
DEBUG: "false"
DJANGO_ALLOWED_HOSTS: "vorgabenportal.knowyoursecurity.com,localhost,127.0.0.1,*"
DJANGO_SETTINGS_MODULE: "VorgabenUI.settings"
# Application Configuration
LANGUAGE_CODE: "de-ch"
TIME_ZONE: "UTC"
# Static and Media Configuration
STATIC_URL: "/static/"
MEDIA_URL: "/media/"
# Application Version
VERSION: "0.990"
# Database Configuration (for future use)
# DATABASE_ENGINE: "django.db.backends.sqlite3"
# DATABASE_NAME: "/app/data/db.sqlite3"
# Security Configuration
# CSRF_TRUSTED_ORIGINS: "https://vorgabenportal.knowyoursecurity.com"
```
#### Deployment Script
The ConfigMap is deployed using the `deploy-argocd-configmap.sh` script located in the `scripts/` directory.
**Script Usage**:
```bash
# Deploy ConfigMap
./scripts/deploy-argocd-configmap.sh
# Verify ConfigMap only (no deployment)
./scripts/deploy-argocd-configmap.sh --verify-only
# Dry-run (show what would be deployed)
./scripts/deploy-argocd-configmap.sh --dry-run
```
**Script Features**:
- Validates kubectl availability
- Checks if ConfigMap file exists
- Creates namespace if it doesn't exist
- Applies ConfigMap configuration
- Verifies successful deployment
- Provides detailed logging and error handling
### Secret Deployment
The Django application requires a secure SECRET_KEY for cryptographic signing. This is managed through a Kubernetes Secret.
#### Secret Configuration
- **Secret Name**: `vorgabenui-secrets`
- **Secret Key**: `vorgabenui_secret`
- **Namespace**: `vorgabenui`
#### Secret Generation
The secret is automatically generated using the `deploy-argocd-secret.sh` script, which creates a secure Django-style SECRET_KEY.
**Script Usage**:
```bash
# Generate and deploy new secret
./scripts/deploy-argocd-secret.sh
# Verify existing secret only (no new generation)
./scripts/deploy-argocd-secret.sh --verify-only
# Dry-run (show what would be done)
./scripts/deploy-argocd-secret.sh --dry-run
```
**Secret Generation Features**:
- Generates secure 50-character SECRET_KEY using Python
- Uses Django-style character set (letters, digits, special characters)
- Creates or updates the secret in the vorgabenui namespace
- Verifies secret deployment and accessibility
- Tests secret accessibility in Django pods
#### Environment Variable Reference
The deployment.yaml references the secret through environment variables:
```yaml
env:
# Secret configuration
- name: VORGABENUI_SECRET
valueFrom:
secretKeyRef:
name: vorgabenui-secrets
key: vorgabenui_secret
```
#### Security Notes
- The SECRET_KEY is never logged or displayed in full
- Only the first 10 characters are shown during generation for verification
- The secret is stored securely in Kubernetes and only accessible to authorized pods
- Regular secret rotation is recommended for production environments
## Deployment Order ## Deployment Order
1. **StorageClass** (`nfs-storageclass.yaml`) - Defines NFS storage class 1. **StorageClass** (`nfs-storageclass.yaml`) - Defines NFS storage class
2. **PersistentVolume** (`nfs-pv.yaml`) - Creates the NFS volume 2. **PersistentVolume** (`nfs-pv.yaml`) - Creates the NFS volume
3. **PersistentVolumeClaim** (`001_pvc.yaml`) - Claims storage for application 3. **PersistentVolumeClaim** (`001_pvc.yaml`) - Claims storage for application
4. **Application Deployments** (`deployment.yaml`, `diagrammer.yaml`) - Deploy application services 4. **ConfigMap** (`configmap.yaml`) - Deploy Django configuration
5. **Ingress** (`ingress.yaml`) - Configure external access 5. **Secret** (`secret.yaml`) - Generate and deploy Django SECRET_KEY
6. **Application Deployments** (`deployment.yaml`, `diagrammer.yaml`) - Deploy application services
7. **Ingress** (`ingress.yaml`) - Configure external access
## Configuration Notes ## Configuration Notes
@@ -227,7 +361,7 @@ kubectl describe pod <pod-name> -n vorgabenui
## Maintenance ## Maintenance
### Backup Strategy ### Backup Strategy
- The NFS server should have regular backups of `/mnt/user/vorgabenui` - The NFS server should have regular backups of `/mnt/user/kubernetesdata/vorgabenui`
- Consider snapshot capabilities if using enterprise NFS solutions - Consider snapshot capabilities if using enterprise NFS solutions
### Monitoring ### Monitoring

View File

@@ -49,7 +49,7 @@ python manage.py import-document Documentation/import\ formats/r009.txt \
### Dry-Run Modus ### Dry-Run Modus
Der Dry-Run Modus ist besonders nützlich zum Testen: Der Dry-Run Modus ist zum Testen gedacht:
```bash ```bash
python manage.py import-document r009.txt \ python manage.py import-document r009.txt \
@@ -73,7 +73,7 @@ python manage.py import-document r009.txt \
--purge --purge
``` ```
Nutzen Sie `--dry-run --purge` zuerst, um zu sehen, was gelöscht würde. Mit `--dry-run --purge` kann zuerst geprüft werden, was gelöscht würde.
## Dateiformat ## Dateiformat
@@ -257,7 +257,7 @@ Abschliessender Text nach der Liste.
>>>Vorgabe [Thema] >>>Vorgabe [Thema]
``` ```
Das Thema muss in der Datenbank bereits als `Thema`-Objekt existieren. Übliche Themen: Das Thema muss in der Datenbank bereits als `Thema`-Objekt existieren. Die bestehenden Themen sind - wie in den bestehenden Standards - aus dem IT-Grundschutz übernommen:
- Organisation - Organisation
- Technik - Technik
- Informationen - Informationen
@@ -275,11 +275,7 @@ Das Thema muss in der Datenbank bereits als `Thema`-Objekt existieren. Übliche
oder inline: oder inline:
``` Die Nummer wird als Integer gespeichert. Sie ist nicht eindeutig innerhalb eines Dokuments und Themas. Wenn mehrere Vorgaben im selben Thema mit der selben Nummer vorkommen, darf sich der Geltungszeitraum der Vorgaben nicht überschneiden (wird beim Import geprüft).
>>>Nummer: 1
```
Die Nummer wird als Integer gespeichert. Sie ist eindeutig innerhalb eines Dokuments und Themas.
#### Titel (Pflicht) #### Titel (Pflicht)
@@ -322,14 +318,6 @@ Komma-getrennte Liste:
Firewall, Netzwerk, Sicherheit Firewall, Netzwerk, Sicherheit
``` ```
oder als Block:
```
>>>Stichworte
>>>Text
Firewall, Netzwerk, Sicherheit
```
**Hinweis:** Stichworte werden automatisch in der Datenbank angelegt, falls sie noch nicht existieren. **Hinweis:** Stichworte werden automatisch in der Datenbank angelegt, falls sie noch nicht existieren.
#### Checkliste (Optional) #### Checkliste (Optional)
@@ -347,7 +335,7 @@ Jede Zeile wird als separate Checklistenfrage gespeichert.
### 1. Dry-Run vor Import ### 1. Dry-Run vor Import
Führen Sie immer zuerst einen Dry-Run durch: Immer zuerst einen Dry-Run durchführen:
```bash ```bash
python manage.py import-document datei.txt \ python manage.py import-document datei.txt \
@@ -359,7 +347,7 @@ python manage.py import-document datei.txt \
### 2. Themen vorab erstellen ### 2. Themen vorab erstellen
Stellen Sie sicher, dass alle verwendeten Themen in der Datenbank existieren: Sicherstellen, dass alle verwendeten Themen in der Datenbank existieren:
```python ```python
python manage.py shell python manage.py shell
@@ -391,11 +379,11 @@ Folgende Abschnitttypen müssen in der Datenbank existieren:
- `code` - `code`
- `diagramm` - `diagramm`
Prüfen Sie diese in der Autorenumgebung unter "Abschnitttypen". Prüfen in der Autorenumgebung unter "Abschnitttypen".
### 5. UTF-8 Kodierung ### 5. UTF-8 Kodierung
Stellen Sie sicher, dass Ihre Importdatei UTF-8 kodiert ist, besonders bei Umlauten (ä, ö, ü) und Sonderzeichen. Sicherstellen, dass die Importdatei UTF-8 kodiert ist, besonders bei Umlauten (ä, ö, ü) und Sonderzeichen.
### 6. Versionierung mit Purge ### 6. Versionierung mit Purge
@@ -436,37 +424,37 @@ Leerzeilen innerhalb eines Abschnitts werden beibehalten. Eine Leerzeile nach ei
Der angegebene Dokumententyp existiert nicht in der Datenbank. Der angegebene Dokumententyp existiert nicht in der Datenbank.
**Lösung:** Erstellen Sie den Dokumententyp in der Autorenumgebung oder per Shell. **Lösung:** Dokumententyp aus dem IT-Grundschutz verwenden, nötigenfalls hinzufügen in der Autorenumgebung oder per Shell.
### "Thema not found, skipping Vorgabe" ### "Thema not found, skipping Vorgabe"
Das in der Vorgabe verwendete Thema existiert nicht. Das in der Vorgabe verwendete Thema existiert nicht.
**Lösung:** Erstellen Sie das Thema in der Autorenumgebung oder passen Sie die Importdatei an. **Lösung:** Thema in der Autorenumgebung erstellen oder Importdatei anpassen.
### "AbschnittTyp not found" ### "AbschnittTyp not found"
Ein verwendeter Abschnitttyp existiert nicht. Ein verwendeter Abschnitttyp existiert nicht.
**Lösung:** **Lösung:**
- Prüfen Sie die Schreibweise (Gross-/Kleinschreibung wird normalisiert) - Schreibweise prüfen (Gross-/Kleinschreibung und "-"/" " wird normalisiert)
- Erstellen Sie den Abschnitttyp in der Autorenumgebung - Wenn nötig Abschnitttyp in der Autorenumgebung erstellen (Achtung! Ausgabeformat muss im Code definiert werden)
- Standardtypen: `text`, `liste geordnet`, `liste ungeordnet` - Standardtypen: `text`, `liste geordnet`, `liste ungeordnet`, `tabelle`, `code`
### Vorgabe wird nicht importiert ### Vorgabe wird nicht importiert
Prüfen Sie: Prüfen:
- Ist `>>>Nummer` gesetzt? - Ist `>>>Nummer` gesetzt?
- Ist `>>>Titel` gesetzt? - Ist `>>>Titel` gesetzt?
- Existiert das Thema? - Existiert das Thema?
Verwenden Sie `--dry-run --verbose` für detaillierte Informationen. `--dry-run --verbose` für detaillierte Informationen.
## Weitere Informationen ## Weitere Informationen
### Beispieldateien ### Beispieldateien
Beispieldateien finden Sie in: Beispieldateien:
- `Documentation/import formats/r009.txt` - `Documentation/import formats/r009.txt`
- `Documentation/import formats/r0126.txt` - `Documentation/import formats/r0126.txt`
@@ -485,7 +473,3 @@ Oder über die Web-Oberfläche: `/dokumente/R0066/?format=json`
- `export_json` - Exportiert Dokumente als JSON - `export_json` - Exportiert Dokumente als JSON
- `sanity_check_vorgaben` - Prüft Vorgaben auf Konflikte - `sanity_check_vorgaben` - Prüft Vorgaben auf Konflikte
- `clear_diagram_cache` - Löscht Diagramm-Cache - `clear_diagram_cache` - Löscht Diagramm-Cache
## Kontakt
Bei Fragen oder Problemen wenden Sie sich an das Information Security Management BIT.

View File

@@ -0,0 +1,5 @@
from django.conf import settings
def version(request):
return {'version': settings.VERSION}

View File

@@ -1,141 +0,0 @@
"""
Django settings for VorgabenUI project.
Generated by 'django-admin startproject' using Django 5.2.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.2/ref/settings/
"""
import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(os.environ.get("DEBUG", default=0))
ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS","127.0.0.1").split(",")
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'dokumente',
'abschnitte',
'stichworte',
'mptt',
'nested_admin',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'VorgabenUI.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'VorgabenUI.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'data/db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/
LANGUAGE_CODE = 'de-ch'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/
STATIC_URL = '/static/'
STATIC_ROOT="/home/adebaumann/VorgabenUI/staticfiles/"
STATICFILES_DIRS= (
os.path.join(BASE_DIR,"static"),
)
# Media files (User-uploaded content)
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# Diagram cache settings
DIAGRAM_CACHE_DIR = 'diagram_cache' # relative to MEDIA_ROOT
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DATA_UPLOAD_MAX_NUMBER_FIELDS=10250
NESTED_ADMIN_LAZY_INLINES = True

View File

@@ -23,6 +23,9 @@ BASE_DIR = Path(__file__).resolve().parent.parent
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 'yes', 'on') DEBUG = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 'yes', 'on')
# Application version (from ConfigMap)
VERSION = os.environ.get('VERSION', '0.0.0')
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get('VORGABENUI_SECRET') SECRET_KEY = os.environ.get('VORGABENUI_SECRET')
if not SECRET_KEY: if not SECRET_KEY:
@@ -94,6 +97,7 @@ TEMPLATES = [
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'VorgabenUI.context_processors.version',
], ],
}, },
}, },
@@ -138,7 +142,7 @@ AUTH_PASSWORD_VALIDATORS = [
LANGUAGE_CODE = 'de-ch' LANGUAGE_CODE = 'de-ch'
TIME_ZONE = 'UTC' TIME_ZONE = 'Europe/Zurich'
USE_I18N = True USE_I18N = True

View File

@@ -41,6 +41,12 @@ urlpatterns = [
] ]
# Serve media files (including cached diagrams) # Serve media files (including cached diagrams)
# django.conf.urls.static.static() is a no-op when DEBUG=False,
# so we wire up the serve view directly for media files.
from django.views.static import serve
urlpatterns += [
re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}),
]
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@@ -13,9 +13,12 @@ data:
LANGUAGE_CODE: "de-ch" LANGUAGE_CODE: "de-ch"
TIME_ZONE: "UTC" TIME_ZONE: "UTC"
# Static and Media Configuration # Static and Media Configuration
STATIC_URL: "/static/" STATIC_URL: "/static/"
MEDIA_URL: "/media/" MEDIA_URL: "/media/"
# Application Version
VERSION: "0.990"
# Database Configuration (for future use) # Database Configuration (for future use)
# DATABASE_ENGINE: "django.db.backends.sqlite3" # DATABASE_ENGINE: "django.db.backends.sqlite3"

View File

@@ -19,13 +19,15 @@ spec:
initContainers: initContainers:
- name: loader - name: loader
image: git.baumann.gr/adebaumann/vui-data-loader:0.11 image: git.baumann.gr/adebaumann/vui-data-loader:0.11
securityContext:
runAsUser: 99
command: [ "sh","-c","if [ ! -f /data/db.sqlite3 ] || [ ! -s /data/db.sqlite3 ]; then cp preload/preload.sqlite3 /data/db.sqlite3 && echo 'Database copied from preload'; else echo 'Existing database preserved'; fi" ] command: [ "sh","-c","if [ ! -f /data/db.sqlite3 ] || [ ! -s /data/db.sqlite3 ]; then cp preload/preload.sqlite3 /data/db.sqlite3 && echo 'Database copied from preload'; else echo 'Existing database preserved'; fi" ]
volumeMounts: volumeMounts:
- name: data - name: data
mountPath: /data mountPath: /data
containers: containers:
- name: web - name: web
image: git.baumann.gr/adebaumann/vui:0.982 image: git.baumann.gr/adebaumann/vui:0.990
imagePullPolicy: Always imagePullPolicy: Always
securityContext: securityContext:
runAsUser: 99 runAsUser: 99
@@ -52,6 +54,11 @@ spec:
configMapKeyRef: configMapKeyRef:
name: django-config name: django-config
key: DJANGO_SETTINGS_MODULE key: DJANGO_SETTINGS_MODULE
- name: VERSION
valueFrom:
configMapKeyRef:
name: django-config
key: VERSION
ports: ports:
- containerPort: 8000 - containerPort: 8000
volumeMounts: volumeMounts:

View File

@@ -15,4 +15,4 @@ spec:
storageClassName: nfs storageClassName: nfs
nfs: nfs:
server: 192.168.17.199 server: 192.168.17.199
path: /mnt/user/vorgabenui path: /mnt/user/kubernetesdata/vorgabenui

512
code-review.md Normal file
View File

@@ -0,0 +1,512 @@
# Code Review: vgui-cicd Django Project
**Date:** January 20, 2026
**Reviewer:** AI Code Review
**Project Path:** /home/adebaumann/development/vgui-cicd
---
## 1. PROJECT STRUCTURE
### Overview
The project follows Django conventions with a clear app structure:
- **VorgabenUI** - Main project settings, URLs, WSGI/ASGI
- **dokumente** - Core document and Vorgabe models (315 lines)
- **abschnitte** - Text section models and utilities
- **stichworte** - Keyword/stichwort models
- **referenzen** - Reference models (MPTT-based)
- **rollen** - Role models
- **pages** - General pages and views
- **diagramm_proxy** - Diagram caching functionality
### Issues Found
**Minor - settings-docker.py**: The `settings-docker.py` file has duplicate `AUTH_PASSWORD_VALIDATORS` definitions (lines 92-105 and 183-199), which is redundant.
- **File**: `/home/adebaumann/development/vgui-cicd/VorgabenUI/settings-docker.py` (lines 92-105 and 183-199)
---
## 2. SETTINGS REVIEW
### Critical Issues
**1. Fallback SECRET_KEY in production** (`/home/adebaumann/development/vgui-cicd/VorgabenUI/settings.py`, lines 27-47)
```python
SECRET_KEY = os.environ.get('VORGABENUI_SECRET')
if not SECRET_KEY:
is_build_env = any([...])
debug_mode = ...
if debug_mode or is_build_env:
SECRET_KEY = 'dev-fallback-key-for-local-debugging-only-not-for-production-use-12345'
```
- **Issue**: Even though there's a check for build environments, the hardcoded fallback key creates a significant security risk if the environment variable is not properly set in production
- **Recommendation**: The fallback should NEVER be enabled, even in development - require the environment variable to be set
**2. DEBUG mode default** (`/home/adebaumann/development/vgui-cicd/VorgabenUI/settings.py`, line 24)
```python
DEBUG = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 'yes', 'on')
```
- **Issue**: DEBUG defaults to True, which could expose sensitive information if environment variables are misconfigured
- **Recommendation**: Require explicit setting of DEBUG to False in production
### Major Issues
**3. ALLOWED_HOSTS with wildcard** (`/home/adebaumann/development/vgui-cicd/VorgabenUI/settings.py`, line 50)
```python
ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', "10.128.128.144,localhost,127.0.0.1,*").split(",")
```
- **Issue**: Default includes `*` which allows any host - dangerous in production
- **Recommendation**: Default should not include wildcard; require explicit configuration
**4. No rate limiting on authentication** (`/home/adebaumann/development/vgui-cicd/VorgabenUI/settings.py`)
- **Issue**: No login throttling or rate limiting configured for authentication endpoints
- **Recommendation**: Add `django-axes` or similar for brute-force protection
**5. SQLite in production** (`/home/adebaumann/development/vgui-cicd/VorgabenUI/settings.py`, lines 109-114)
```python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'data/db.sqlite3',
}
}
```
- **Issue**: SQLite is used as default database - not suitable for production with concurrent access
- **Recommendation**: Configure PostgreSQL or other production-ready database
### Minor Issues
**6. TIME_ZONE mismatch** (`/home/adebaumann/development/vgui-cicd/VorgabenUI/settings.py`, lines 139-145)
- **Issue**: `LANGUAGE_CODE = 'de-ch'` but `TIME_ZONE = 'UTC'` - timezone should probably be 'Europe/Zurich' for Swiss deployment
- **File**: `/home/adebaumann/development/vgui-cicd/VorgabenUI/settings.py` (lines 141)
---
## 3. MODELS REVIEW
### Major Issues
**1. Missing `verbose_name` on models** - Several models are missing proper `verbose_name` in their Meta class:
- `Stichwort` (`/home/adebaumann/development/vgui-cicd/stichworte/models.py`, lines 4-11) - missing verbose_name
- `Referenz` (`/home/adebaumann/development/vgui-cicd/referenzen/models.py`, lines 6-27) - missing verbose_name
- `Rolle` (`/home/adebaumann/development/vgui-cicd/rollen/models.py`, lines 5-11) - missing verbose_name
- `Person` (`/home/adebaumann/development/vgui-cicd/dokumente/models.py`, lines 22-30) - missing verbose_name
- `Thema` (`/home/adebaumann/development/vgui-cicd/dokumente/models.py`, lines 32-39) - missing verbose_name
**2. BooleanField with `blank=True` but no default** (`/home/adebaumann/development/vgui-cicd/dokumente/models.py`, line 52)
```python
aktiv = models.BooleanField(blank=True)
```
- **Issue**: BooleanField should have explicit `default=False` for clarity
- **Recommendation**: Add `default=False`
**3. No database constraints for date validation** (`/home/adebaumann/development/vgui-cicd/dokumente/models.py`, lines 96-97)
```python
gueltigkeit_von = models.DateField()
gueltigkeit_bis = models.DateField(blank=True,null=True)
```
- **Issue**: No database-level constraint ensuring `gueltigkeit_bis >= gueltigkeit_von`
- **Recommendation**: Add constraint validation or override `save()` method
### Minor Issues
**4. Inconsistent naming in foreign key fields** - Some models use plural related names inconsistently:
- `autoren` and `pruefende` on `Dokument` use plural (correct)
- Could consider singular `related_name` for consistency where applicable
**5. Missing `related_name` on some ManyToMany fields** (`/home/adebaumann/development/vgui-cicd/dokumente/models.py`, line 289)
```python
autoren = models.ManyToManyField(Person) # Missing related_name
```
- **Recommendation**: Add `related_name='changelog_entries'` for clarity
---
## 4. VIEWS REVIEW
### Critical Issues
**1. No CSRF protection on comment endpoints** (`/home/adebaumann/development/vgui-cicd/dokumente/views.py`, lines 321-377)
```python
@require_POST
@login_required
def add_vorgabe_comment(request, vorgabe_id):
# No @csrf_exempt but also no CSRF token verification in the view
```
- **Issue**: The view uses `@require_POST` but doesn't verify CSRF tokens for the JSON endpoint
- **Recommendation**: Add `@csrf_exempt` ONLY if intentionally bypassing, or ensure CSRF is handled via the X-CSRFToken header (which is done in the template)
**Note**: Looking at line 368, the template sends `'X-CSRFToken': getCookie('csrftoken')`, so this is actually properly handled. **Not a bug.**
**2. XSS in comment display** (`/home/adebaumann/development/vgui-cicd/dokumente/views.py`, lines 305-306)
```python
escaped_text = escape(comment.text).replace('\n', '<br>')
```
- **Issue**: Using `escape()` is good, but line breaks are converted to `<br>` which could still be exploited
- **Note**: The `dangerous_patterns` check at lines 339-343 provides some protection
- **Recommendation**: Consider using a more robust HTML sanitization library
**3. No input validation on comment length** (`/home/adebaumann/development/vgui-cicd/dokumente/views.py`, line 335)
```python
if len(text) > 2000: # Reasonable length limit
```
- **Issue**: This is actually properly implemented with a 2000 character limit
- **Status**: OK
### Major Issues
**4. Referenz view lacks error handling** (`/home/adebaumann/development/vgui-cicd/referenzen/views.py`, lines 11-19)
```python
def detail(request, refid):
referenz_item = Referenz.objects.get(id=refid)
```
- **Issue**: `DoesNotExist` exception not caught - will return 500 error instead of 404
- **Recommendation**: Use `get_object_or_404` for consistency
### Minor Issues
**5. Search view allows complex regex patterns** (`/home/adebaumann/development/vgui-cicd/pages/views.py`, lines 36-70)
- **Issue**: The validation is good but the `groupby` usage at line 54 could fail if data is not properly sorted
- **Recommendation**: Add explicit ordering before groupby
**6. No rate limiting on search** (`/home/adebaumann/development/vgui-cicd/pages/views.py`)
- **Issue**: Search endpoint could be abused for DoS
- **Recommendation**: Add rate limiting
---
## 5. URL CONFIGURATION
### Minor Issues
**1. No URL namespace for include** (`/home/adebaumann/development/vgui-cicd/VorgabenUI/urls.py`, lines 31-35)
```python
path('dokumente/', include("dokumente.urls")),
path('stichworte/', include("stichworte.urls")),
```
- **Issue**: No `app_name` namespace defined in included apps
- **Recommendation**: Add `app_name = 'dokumente'` to `dokumente/urls.py` for cleaner reversals
**2. Inconsistent trailing slashes** - Most URLs have trailing slashes but not all - ensure consistency
---
## 6. TEMPLATES REVIEW
### Critical Issues
**1. XSS vulnerability in `standard_detail.html`** (`/home/adebaumann/development/vgui-cicd/dokumente/templates/standards/standard_detail.html`, lines 163-164)
```html
{% for ref in vorgabe.referenzpfade %}
{{ ref|safe }}{% if not forloop.last %}, {% endif %}
```
- **Issue**: Using `|safe` on reference paths could allow XSS if malicious content is stored
- **Recommendation**: Use `escape` filter and handle line breaks separately
**2. JavaScript in template without CSP** (`/home/adebaumann/development/vgui-cicd/dokumente/templates/standards/standard_detail.html`, lines 249-424)
- **Issue**: Inline JavaScript in template
- **Note**: CSP headers are set in the view (line 316-317), but inline scripts violate strict CSP
- **Recommendation**: Move JavaScript to external file
### Major Issues
**3. Missing ARIA labels and roles** - Several accessibility issues:
- Base template (`/home/adebaumann/development/vgui-cicd/pages/templates/base.html`) has navigation but missing `aria-label` on some elements
- The mobile navigation could use better ARIA attributes
**4. Missing alt attributes on images** (`/home/adebaumann/development/vgui-cicd/pages/templates/base.html`, lines 39-41)
```html
<img src="{% static 'swiss/img/logo-CH.svg' %}"
onerror="this.onerror=null; this.src='{% static 'swiss/img/logo-CH.png' %}'"
alt="Zur Startseite" />
```
- **Issue**: alt is present but could be more descriptive
- **Status**: Acceptable
### Minor Issues
**5. Hardcoded URLs in templates** (`/home/adebaumann/development/vgui-cicd/dokumente/templates/standards/incomplete_vorgaben.html`, line 21)
```html
<a href="/autorenumgebung/dokumente/vorgabe/{{ item.vorgabe.id }}/change/"
```
- **Issue**: Hardcoded admin URL instead of using URL reversal
- **Recommendation**: Use `{% url 'admin:dokumente_vorgabe_change' item.vorgabe.id %}`
---
## 7. FORMS REVIEW
### Minor Issues
**1. No dedicated form classes** - Most form handling is done via Django admin forms or directly in views
- **Recommendation**: Consider creating explicit `ModelForm` classes for views that accept user input (e.g., comment form)
**2. VorgabeForm in admin could have more validation** (`/home/adebaumann/development/vgui-cicd/dokumente/admin.py`, lines 95-107)
- **Issue**: The form only validates Thema is required
- **Recommendation**: Add validation for date ranges and conflicts
---
## 8. MANAGEMENT COMMANDS
### Major Issues
**1. import-document command lacks error recovery** (`/home/adebaumann/development/vgui-cicd/dokumente/management/commands/import-document.py`, lines 288-349)
```python
for v in vorgaben_data:
try:
thema = Thema.objects.get(name=v["thema"])
except Thema.DoesNotExist:
self.stdout.write(self.style.WARNING(...))
continue # Silently skips vorgabe
```
- **Issue**: If one Vorgabe fails, the entire command may leave partial data
- **Recommendation**: Use `transaction.atomic()` to ensure atomicity
**2. No progress indicator for large imports** (`/home/adebaumann/development/vgui-cicd/dokumente/management/commands/import-document.py`)
- **Issue**: For large files, no progress shown
- **Recommendation**: Add progress output
### Minor Issues
**3. export_json command hardcodes "Standard IT-Sicherheit"** (`/home/adebaumann/development/vgui-cicd/dokumente/management/commands/export_json.py`, line 30)
```python
result = {
"Vorgabendokument": {
"Typ": "Standard IT-Sicherheit",
```
- **Issue**: Typ is hardcoded instead of using `dokument.dokumententyp.name`
- **Recommendation**: Use actual dokumententyp
---
## 9. TESTS REVIEW
### Major Issues
**1. No tests for diagram caching** - The `diagramm_proxy` module has no test coverage
- **Recommendation**: Add tests for `diagram_cache.py`
**2. No tests for referenzen views** - The tree view and detail view have no test coverage
- **Recommendation**: Add tests for `referenzen/views.py`
**3. No tests for authentication security** - Missing tests for:
- Brute-force protection
- Session management
- Password policy enforcement
### Minor Issues
**4. Test file organization** - `test_json.py` should be part of `tests.py` or in a proper test package structure
- **Status**: Acceptable but could be improved
**5. Tests rely on hardcoded paths** (`/home/adebaumann/development/vgui-cicd/dokumente/tests.py`, lines 1165-1168)
```python
self.assertContains(response, 'href="/autorenumgebung/dokumente/vorgabe/2/change/"')
```
- **Issue**: Uses hardcoded URL paths instead of URL reversal
- **Recommendation**: Use `reverse('admin:dokumente_vorgabe_change', args=[vorgabe.pk])`
---
## 10. SECURITY REVIEW
### Critical Issues
**1. No rate limiting on any endpoint** - All views lack rate limiting
- **Recommendation**: Add `django-ratelimit` or similar
**2. Diagram cache potentially vulnerable to DoS** (`/home/adebaumann/development/vgui-cicd/diagramm_proxy/diagram_cache.py`, lines 24-67)
```python
def get_cached_diagram(diagram_type, diagram_content):
content_hash = compute_hash(diagram_content)
cache_path = get_cache_path(diagram_type, content_hash)
if default_storage.exists(cache_path):
return cache_path
# Generate diagram via POST request
url = f"{KROKI_UPSTREAM}/{diagram_type}/svg"
```
- **Issue**: No validation on diagram size or content - could lead to DoS via large diagrams
- **Recommendation**: Add size limits and timeout
### Major Issues
**3. CSRF trusted origins only for HTTPS** (`/home/adebaumann/development/vgui-cicd/VorgabenUI/settings.py`, line 104)
```python
CSRF_TRUSTED_ORIGINS=["https://vorgabenportal.knowyoursecurity.com"]
```
- **Issue**: Only one origin configured - ensure this covers all deployment URLs
- **Recommendation**: Make configurable via environment variable
**4. No session expiry configuration** (`/home/adebaumann/development/vgui-cicd/VorgabenUI/settings.py`)
- **Issue**: Sessions don't expire
- **Recommendation**: Set `SESSION_COOKIE_AGE` and `SESSION_SAVE_EVERY_REQUEST`
---
## 11. CODE STYLE COMPLIANCE (AGENTS.md)
### Violations Found
**1. Import order inconsistent** (`/home/adebaumann/development/vgui-cicd/dokumente/views.py`, lines 1-16)
- Imports are not strictly ordered (stdlib, Django, local apps)
- Example: `import parsedatetime` is placed after Django imports
**2. Missing German `verbose_name` on models** (as noted in Section 3)
**3. Function naming** (`/home/adebaumann/development/vgui-cicd/dokumente/models.py`, line 101)
```python
def Vorgabennummer(self): # Should be vorgabennummer (snake_case)
```
- **Issue**: Method name uses PascalCase instead of snake_case per AGENTS.md guidelines
**4. Typo in error message** (`/home/adebaumann/development/vgui-cicd/dokumente/models.py`, line 213)
```python
"Geltungsdauer übeschneidet sich" # Should be "überschneidet sich"
```
---
## 12. PERFORMANCE ISSUES
### Major Issues
**1. N+1 query potential in search** (`/home/adebaumann/development/vgui-cicd/pages/views.py`, lines 53-68)
```python
qs = VorgabeKurztext.objects.filter(inhalt__icontains=suchbegriff)...
```
- **Issue**: Uses `icontains` which cannot use database indexes effectively
- **Recommendation**: Consider PostgreSQL full-text search for better performance
**2. No select_related in referenzen views** (`/home/adebaumann/development/vgui-cicd/referenzen/views.py`, lines 7-8)
```python
def tree(request):
referenz_items = Referenz.objects.all()
```
- **Issue**: No prefetch_related for related data
- **Recommendation**: Add prefetch_related for `referenzerklaerung_set` and `unterreferenzen`
---
## SUMMARY
### Critical Issues (Must Fix)
| # | Issue | Location | Recommendation |
|---|-------|----------|----------------|
| 1 | SECRET_KEY fallback | `VorgabenUI/settings.py:27-47` | Never enable fallback, require env var |
| 2 | DEBUG defaults to True | `VorgabenUI/settings.py:24` | Require explicit False for production |
| 3 | No rate limiting | All views | Add django-ratelimit |
| 4 | Session never expires | `VorgabenUI/settings.py` | Set SESSION_COOKIE_AGE |
| 5 | XSS via `\|safe` filter | `standard_detail.html:163-164` | Use `escape` filter |
### Major Issues (Should Fix)
| # | Issue | Location | Recommendation |
|---|-------|----------|----------------|
| 1 | SQLite database | `VorgabenUI/settings.py:109-114` | Use PostgreSQL for production |
| 2 | ALLOWED_HOSTS wildcard | `VorgabenUI/settings.py:50` | Remove `*` from default |
| 3 | Missing date constraint | `dokumente/models.py:96-97` | Add validation for date ranges |
| 4 | Import not atomic | `import-document.py:288-349` | Wrap in transaction.atomic() |
| 5 | Missing test coverage | Multiple modules | Add tests for untested code |
### Minor Issues (Nice to Fix)
| # | Issue | Location |
|---|-------|----------|
| 1 | Code style violations | `dokumente/views.py:1-16` |
| 2 | Typo: "übeschneidet" | `dokumente/models.py:213` |
| 3 | Missing ARIA labels | `base.html` |
| 4 | Hardcoded URLs in templates | `incomplete_vorgaben.html:21` |
| 5 | Duplicate AUTH_PASSWORD_VALIDATORS | `settings-docker.py:92-105, 183-199` |
---
## RECOMMENDED ACTIONS
### Immediate (Before Production)
1. Set up proper SECRET_KEY via environment variable
2. Configure DEBUG=False explicitly
3. Remove wildcard from ALLOWED_HOSTS default
4. Add rate limiting to all endpoints
5. Configure session expiry
6. Fix XSS vulnerability in template
7. Switch to PostgreSQL database
### Short-term (1-2 Weeks)
1. Add database constraints for date validation
2. Wrap import command in transactions
3. Add missing verbose_name to models
4. Fix code style violations
5. Add test coverage for critical paths
6. Move inline JavaScript to external files
### Long-term (1 Month)
1. Implement PostgreSQL full-text search
2. Add comprehensive test suite
3. Set up CSP headers properly
4. Implement comprehensive authentication security
5. Add performance monitoring
6. Document all security configurations
---
## POSITIVE FINDINGS
1. **Good project organization** - Clear app structure following Django conventions
2. **Proper CSRF handling** - X-CSRFToken header properly implemented
3. **Input validation** - Comment length limits and dangerous pattern checks in place
4. **Good German localization** - German field names and verbose texts throughout
5. **Django admin integration** - Well-configured admin interface
6. **Management commands** - Useful import/export functionality
7. **Referenzen tree structure** - MPTT implementation for hierarchical data
---
## APPENDIX: Additional Issues Detected by LSP
The following issues were detected by the Language Server Protocol (LSP) analyzer:
### dokumente/models.py
| Line | Issue |
|------|-------|
| 14, 26, 36, 274 | `__str__` method return type mismatch - returns `CharField` instead of `str` |
| 70 | Unknown attribute `vorgaben` on `Dokument` |
| 102 | Unknown attribute `nummer` on `ForeignKey` |
| 106, 114 | Unknown attribute `strftime` on `DateField` |
| 137, 144, 196 | Unknown attribute `objects` on `type[Vorgabe]` |
| 173 | Unknown attribute `thema_id` on `Vorgabe` |
| 248, 254, 260, 266 | `Meta` class overrides incompatible parent class |
| 282 | `VorgabenTable.Meta` incompatible with `Vorgabe.Meta` |
| 294 | Unknown attribute `nummer` on `ForeignKey` |
| 314 | Unknown attributes `username` and `Vorgabennummer` on `ForeignKey` |
### dokumente/views.py
| Line | Issue |
|------|-------|
| 22, 133, 265 | Unknown attribute `objects` on `type[Dokument]` |
| 94 | Unknown attribute `objects` on `type[Vorgabe]` |
| 285 | Argument type `str` cannot be assigned to parameter `content` of type `bytes` |
| 345, 412, 440 | Unknown attribute `objects` on `type[VorgabeComment]` |
### abschnitte/models.py
| Line | Issue |
|------|-------|
| 6 | `__str__` method return type mismatch |
| 18 | Argument type `Literal[0]` cannot be assigned to parameter `default` |
### referenzen/models.py
| Line | Issue |
|------|-------|
| 23 | `__str__` method return type mismatch |
| 26 | `Referenz.Meta` overrides incompatible parent `MPTTModel.Meta` |
| 32 | `Referenzerklaerung.Meta` overrides incompatible parent `Textabschnitt.Meta` |
### rollen/models.py
| Line | Issue |
|------|-------|
| 8 | `__str__` method return type mismatch |
| 15 | `RollenBeschreibung.Meta` overrides incompatible parent `Textabschnitt.Meta` |
---
*End of Code Review*

View File

@@ -1,16 +1,10 @@
from django.contrib import admin from django.contrib import admin
#from nested_inline.admin import NestedStackedInline, NestedModelAdmin
from nested_admin import NestedStackedInline, NestedModelAdmin, NestedTabularInline
from django import forms from django import forms
from django.utils.html import format_html from django.utils.safestring import mark_safe
from mptt.forms import TreeNodeMultipleChoiceField
from mptt.admin import DraggableMPTTAdmin
from adminsortable2.admin import SortableInlineAdminMixin, SortableAdminBase
# Register your models here. from nested_admin import NestedStackedInline, NestedModelAdmin, NestedTabularInline
from .models import * from .models import *
from stichworte.models import Stichwort, Stichworterklaerung from stichworte.models import Stichwort, Stichworterklaerung
from referenzen.models import Referenz
@@ -69,7 +63,6 @@ class GeltungsbereichInline(NestedStackedInline):
extra=0 extra=0
sortable_field_name = "order" sortable_field_name = "order"
show_change_link=True show_change_link=True
classes = ['collapse']
verbose_name_plural = "Geltungsbereich-Abschnitte" verbose_name_plural = "Geltungsbereich-Abschnitte"
fieldsets = ( fieldsets = (
(None, { (None, {
@@ -83,7 +76,6 @@ class EinleitungInline(NestedStackedInline):
extra = 0 extra = 0
sortable_field_name = "order" sortable_field_name = "order"
show_change_link = True show_change_link = True
classes = ['collapse']
verbose_name_plural = "Einleitungs-Abschnitte" verbose_name_plural = "Einleitungs-Abschnitte"
fieldsets = ( fieldsets = (
(None, { (None, {
@@ -93,12 +85,10 @@ class EinleitungInline(NestedStackedInline):
) )
class VorgabeForm(forms.ModelForm): class VorgabeForm(forms.ModelForm):
referenzen = TreeNodeMultipleChoiceField(queryset=Referenz.objects.all(), required=False)
class Meta: class Meta:
model = Vorgabe model = Vorgabe
fields = '__all__' fields = '__all__'
def clean_thema(self): def clean_thema(self):
"""Validate that thema is provided.""" """Validate that thema is provided."""
thema = self.cleaned_data.get('thema') thema = self.cleaned_data.get('thema')
@@ -106,7 +96,7 @@ class VorgabeForm(forms.ModelForm):
raise forms.ValidationError('Thema ist ein Pflichtfeld. Bitte wählen Sie ein Thema aus.') raise forms.ValidationError('Thema ist ein Pflichtfeld. Bitte wählen Sie ein Thema aus.')
return thema return thema
class VorgabeInline(SortableInlineAdminMixin, NestedStackedInline): class VorgabeInline(NestedStackedInline):
model = Vorgabe model = Vorgabe
form = VorgabeForm form = VorgabeForm
extra = 0 extra = 0
@@ -165,7 +155,7 @@ class StichwortAdmin(NestedModelAdmin):
count = len(vorgaben_list) count = len(vorgaben_list)
if count == 0: if count == 0:
return format_html("<em>Keine Vorgaben gefunden</em><p><strong>Gesamt: 0 Vorgaben</strong></p>") return mark_safe("<em>Keine Vorgaben gefunden</em><p><strong>Gesamt: 0 Vorgaben</strong></p>")
html = "<div style='max-height: 300px; overflow-y: auto;'>" html = "<div style='max-height: 300px; overflow-y: auto;'>"
html += "<table style='width: 100%; border-collapse: collapse;'>" html += "<table style='width: 100%; border-collapse: collapse;'>"
@@ -186,7 +176,7 @@ class StichwortAdmin(NestedModelAdmin):
html += "</tbody></table>" html += "</tbody></table>"
html += f"</div><p><strong>Gesamt: {count} Vorgabe{'n' if count != 1 else ''}</strong></p>" html += f"</div><p><strong>Gesamt: {count} Vorgabe{'n' if count != 1 else ''}</strong></p>"
return format_html(html) return mark_safe(html)
vorgaben_list.short_description = "Zugeordnete Vorgaben" vorgaben_list.short_description = "Zugeordnete Vorgaben"
def get_queryset(self, request): def get_queryset(self, request):
@@ -204,7 +194,7 @@ class PersonAdmin(admin.ModelAdmin):
@admin.register(Dokument) @admin.register(Dokument)
class DokumentAdmin(SortableAdminBase, NestedModelAdmin): class DokumentAdmin(NestedModelAdmin):
actions_on_top=True actions_on_top=True
inlines = [EinleitungInline, GeltungsbereichInline, VorgabeInline] inlines = [EinleitungInline, GeltungsbereichInline, VorgabeInline]
filter_horizontal=['autoren','pruefende'] filter_horizontal=['autoren','pruefende']

View File

@@ -0,0 +1,34 @@
# Generated by Django 6.0.1 on 2026-02-12 13:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dokumente', '0010_vorgabentable_alter_person_options_vorgabecomment'),
]
operations = [
migrations.AlterModelOptions(
name='einleitung',
options={'ordering': ('order',), 'verbose_name': 'Einleitungs-Abschnitt', 'verbose_name_plural': 'Einleitung'},
),
migrations.AlterModelOptions(
name='geltungsbereich',
options={'ordering': ('order',), 'verbose_name': 'Geltungsbereichs-Abschnitt', 'verbose_name_plural': 'Geltungsbereich'},
),
migrations.AlterModelOptions(
name='person',
options={'ordering': ['name'], 'verbose_name': 'Person', 'verbose_name_plural': 'Personen'},
),
migrations.AlterModelOptions(
name='thema',
options={'verbose_name': 'Thema', 'verbose_name_plural': 'Themen'},
),
migrations.AlterField(
model_name='dokument',
name='aktiv',
field=models.BooleanField(blank=True, default=False),
),
]

View File

@@ -0,0 +1,21 @@
# Generated by Django 6.0.1 on 2026-02-12 13:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dokumente', '0011_alter_einleitung_options_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='einleitung',
options={'ordering': ['order'], 'verbose_name': 'Einleitungs-Abschnitt', 'verbose_name_plural': 'Einleitung'},
),
migrations.AlterModelOptions(
name='geltungsbereich',
options={'ordering': ['order'], 'verbose_name': 'Geltungsbereichs-Abschnitt', 'verbose_name_plural': 'Geltungsbereich'},
),
]

View File

@@ -12,7 +12,7 @@ class Dokumententyp(models.Model):
verantwortliche_ve = models.CharField(max_length=255) verantwortliche_ve = models.CharField(max_length=255)
def __str__(self): def __str__(self):
return self.name return str(self.name)
class Meta: class Meta:
verbose_name="Dokumententyp" verbose_name="Dokumententyp"
@@ -28,6 +28,7 @@ class Person(models.Model):
class Meta: class Meta:
verbose_name_plural="Personen" verbose_name_plural="Personen"
ordering = ['name'] ordering = ['name']
verbose_name="Person"
class Thema(models.Model): class Thema(models.Model):
name = models.CharField(max_length=100, primary_key=True) name = models.CharField(max_length=100, primary_key=True)
@@ -37,7 +38,7 @@ class Thema(models.Model):
return self.name return self.name
class Meta: class Meta:
verbose_name_plural="Themen" verbose_name_plural="Themen"
verbose_name="Thema"
class Dokument(models.Model): class Dokument(models.Model):
nummer = models.CharField(max_length=50, primary_key=True) nummer = models.CharField(max_length=50, primary_key=True)
@@ -49,7 +50,7 @@ class Dokument(models.Model):
gueltigkeit_bis = models.DateField(null=True, blank=True) gueltigkeit_bis = models.DateField(null=True, blank=True)
signatur_cso = models.CharField(max_length=255, blank=True) signatur_cso = models.CharField(max_length=255, blank=True)
anhaenge = models.TextField(blank=True) anhaenge = models.TextField(blank=True)
aktiv = models.BooleanField(blank=True) aktiv = models.BooleanField(blank=True,default=False)
def __str__(self): def __str__(self):
return f"{self.nummer} {self.name}" return f"{self.nummer} {self.name}"
@@ -260,12 +261,14 @@ class Geltungsbereich(Textabschnitt):
class Meta: class Meta:
verbose_name_plural="Geltungsbereich" verbose_name_plural="Geltungsbereich"
verbose_name="Geltungsbereichs-Abschnitt" verbose_name="Geltungsbereichs-Abschnitt"
ordering = ['order']
class Einleitung(Textabschnitt): class Einleitung(Textabschnitt):
einleitung=models.ForeignKey(Dokument,on_delete=models.CASCADE) einleitung=models.ForeignKey(Dokument,on_delete=models.CASCADE)
class Meta: class Meta:
verbose_name_plural="Einleitung" verbose_name_plural="Einleitung"
verbose_name="Einleitungs-Abschnitt" verbose_name="Einleitungs-Abschnitt"
ordering = ['order']
class Checklistenfrage(models.Model): class Checklistenfrage(models.Model):
vorgabe=models.ForeignKey(Vorgabe, on_delete=models.CASCADE, related_name="checklistenfragen") vorgabe=models.ForeignKey(Vorgabe, on_delete=models.CASCADE, related_name="checklistenfragen")

View File

@@ -15,6 +15,7 @@
<h2>Checkliste</h2> <h2>Checkliste</h2>
<p>Die Checkliste wird unter anderem für Audits oder Prüfungen benutzt. Wir empfehlen allen Verantwortlichen, sie als internes Qualitätssicherungs-Hilfsmittel zu verwenden.</p>
<ul class="list-group"> <ul class="list-group">
{% for vorgabe in vorgaben %} {% for vorgabe in vorgaben %}
{% if vorgabe.checklistenfragen.all %} {% if vorgabe.checklistenfragen.all %}
@@ -25,4 +26,4 @@
{% endfor %} {% endfor %}
</ul> </ul>
</body> </body>
</html> </html>

View File

@@ -1,19 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: data-loader
namespace: vorgabenui
spec:
restartPolicy: Never
containers:
- name: loader
image: adebaumann/vgui-preloader:0.5
command: ["sh","-c","cp -v --debug --update=none /preload/preload.sqlite3 /data/db.sqlite3; chown -R 999:999 /data; ls -la /data; exit 0"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: django-data-pvc

Binary file not shown.

View File

@@ -1,68 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: django
namespace: vorgabenui
spec:
replicas: 10
selector:
matchLabels:
app: django
template:
metadata:
labels:
app: django
spec:
securityContext:
fsGroup: 999
fsGroupChangePolicy: "OnRootMismatch"
initContainers:
- name: loader
image: adebaumann/vgui-preloader:0.5
command: [ "sh","-c","cp -v --debug --update=none /preload/preload.sqlite3 /data/db.sqlite3; chown -R 999:999 /data; ls -la /data; exit 0" ]
volumeMounts:
- name: data
mountPath: /data
containers:
- name: web
image: docker.io/adebaumann/vui:0.918
imagePullPolicy: Always
ports:
- containerPort: 8000
volumeMounts:
- name: data
mountPath: /app/data
readinessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 6
livenessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 20
periodSeconds: 20
timeoutSeconds: 2
failureThreshold: 3
volumes:
- name: data
persistentVolumeClaim:
claimName: django-data-pvc
---
apiVersion: v1
kind: Service
metadata:
name: django
namespace: vorgabenui
spec:
type: ClusterIP
selector:
app: django
ports:
- port: 8000
targetPort: 8000

View File

@@ -1,60 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: kroki
namespace: vorgabenui
spec:
replicas: 1
selector:
matchLabels:
app: kroki
template:
metadata:
labels:
app: kroki
spec:
containers:
- name: kroki
image: docker.io/yuzutech/kroki:latest
ports:
- containerPort: 8000
readinessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 6
livenessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 20
periodSeconds: 20
timeoutSeconds: 2
failureThreshold: 3
- name: mermaid
image: docker.io/yuzutech/kroki-mermaid:latest
ports:
- containerPort: 8002
- name: bpmn
image: docker.io/yuzutech/kroki-bpmn:latest
ports:
- containerPort: 8003
- name: excalidraw
image: docker.io/yuzutech/kroki-excalidraw:latest
ports:
- containerPort: 8004
---
apiVersion: v1
kind: Service
metadata:
name: svckroki
namespace: vorgabenui
spec:
selector:
app: kroki
ports:
- port: 8000
targetPort: 8000

View File

@@ -1,52 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: vgui-cicd-django
namespace: vorgabenui
spec:
replicas: 1
selector:
matchLabels:
app: vgui-cicd-django
template:
metadata:
labels:
app: vgui-cicd-django
spec:
containers:
- name: django
image: your-django-image:latest
ports:
- containerPort: 8000
env:
# Django SECRET_KEY from Kubernetes secret
- name: VORGABENUI_SECRET
valueFrom:
secretKeyRef:
name: vorgabenui-secrets
key: vorgabenui_secret
# Other environment variables can be added here
- name: DEBUG
value: "False"
# Add database configuration, etc.
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: vgui-cicd-django-service
namespace: vorgabenui
spec:
selector:
app: vgui-cicd-django
ports:
- protocol: TCP
port: 80
targetPort: 8000
type: ClusterIP

View File

@@ -1,9 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: vorgabenui-secrets
namespace: vorgabenui
type: Opaque
data:
# Base64 encoded SECRET_KEY - will be populated by deployment script
vorgabenui_secret: ""

View File

@@ -1,19 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: django
namespace: vorgabenui
annotations:
traefik.ingress.kubernetes.io/router.middlewares: "vorgabenui-vorgabenui-rewrite@kubernetescrd"
spec:
rules:
- host: vorgabenui.adebaumann.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: django
port:
number: 8000

View File

@@ -1,18 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: django
namespace: vorgabenui
spec:
ingressClassName: nginx
rules:
- host: vorgabenui.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: django
port:
number: 8000

View File

@@ -1,15 +0,0 @@
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

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

View File

@@ -1,13 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: django-data-pvc
namespace: vorgabenui
spec:
accessModes:
- ReadWriteMany
storageClassName: nfs
resources:
requests:
storage: 2Gi

View File

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

View File

@@ -219,7 +219,7 @@
</p> </p>
</div> </div>
<div class="col-sm-6 text-right"> <div class="col-sm-6 text-right">
<p class="text-muted">Version {{ version|default:"0.980" }}</p> <p class="text-muted">Version {{ version|default:"0.986" }}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html from django.utils.safestring import mark_safe
from nested_admin import NestedStackedInline, NestedModelAdmin, NestedTabularInline from nested_admin import NestedStackedInline, NestedModelAdmin, NestedTabularInline
from .models import * from .models import *
@@ -14,7 +14,7 @@ class ReferenzerklaerungInline(NestedStackedInline):
class ReferenzAdmin(NestedModelAdmin): class ReferenzAdmin(NestedModelAdmin):
list_display = ('Path', 'vorgaben_count') list_display = ('Path', 'vorgaben_count')
inlines=[ReferenzerklaerungInline] inlines=[ReferenzerklaerungInline]
search_fields=("name_nummer","Path") search_fields=("name_nummer","name_text")
readonly_fields=("vorgaben_list",) readonly_fields=("vorgaben_list",)
fieldsets = ( fieldsets = (
(None, { (None, {
@@ -34,7 +34,7 @@ class ReferenzAdmin(NestedModelAdmin):
vorgaben_list = list(vorgaben) # Evaluate queryset once vorgaben_list = list(vorgaben) # Evaluate queryset once
count = len(vorgaben_list) count = len(vorgaben_list)
if count == 0: if count == 0:
return format_html("<em>Keine Vorgaben gefunden</em><p><strong>Gesamt: 0 Vorgaben</strong></p>") return mark_safe("<em>Keine Vorgaben gefunden</em><p><strong>Gesamt: 0 Vorgaben</strong></p>")
html = "<div style='max-height: 300px; overflow-y: auto;'>" html = "<div style='max-height: 300px; overflow-y: auto;'>"
html += "<table style='width: 100%; border-collapse: collapse;'>" html += "<table style='width: 100%; border-collapse: collapse;'>"
@@ -55,7 +55,7 @@ class ReferenzAdmin(NestedModelAdmin):
html += "</tbody></table>" html += "</tbody></table>"
html += f"</div><p><strong>Gesamt: {count} Vorgabe{'n' if count != 1 else ''}</strong></p>" html += f"</div><p><strong>Gesamt: {count} Vorgabe{'n' if count != 1 else ''}</strong></p>"
return format_html(html) return mark_safe(html)
vorgaben_list.short_description = "Zugeordnete Vorgaben" vorgaben_list.short_description = "Zugeordnete Vorgaben"

View File

@@ -0,0 +1,17 @@
# Generated by Django 6.0.1 on 2026-01-20 08:57
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('referenzen', '0003_alter_referenzerklaerung_options'),
]
operations = [
migrations.AlterModelOptions(
name='referenz',
options={'verbose_name': 'Referenz', 'verbose_name_plural': 'Referenzen'},
),
]

View File

@@ -13,18 +13,21 @@ class Referenz(MPTTModel):
url = models.URLField(blank=True) url = models.URLField(blank=True)
def Path(self): def Path(self):
Temp = "".join([str(x) for x in self.get_ancestors(include_self=True)])+(" (%s)"%self.name_text if self.name_text else "") path = "".join([x.name_nummer for x in self.get_ancestors(include_self=True)])
return Temp if self.name_text:
path += " (%s)" % self.name_text
return path
class MPTTMeta: class MPTTMeta:
parent_attr = 'oberreferenz' # optional, but safe parent_attr = 'oberreferenz' # optional, but safe
order_insertion_by = ['name_nummer'] order_insertion_by = ['name_nummer']
def __str__(self): def __str__(self):
return self.name_nummer return self.Path()
class Meta: class Meta:
verbose_name_plural="Referenzen" verbose_name_plural="Referenzen"
verbose_name="Referenz"
class Referenzerklaerung (Textabschnitt): class Referenzerklaerung (Textabschnitt):
erklaerung = models.ForeignKey(Referenz,on_delete=models.CASCADE) erklaerung = models.ForeignKey(Referenz,on_delete=models.CASCADE)

View File

@@ -1,6 +1,7 @@
from django.shortcuts import render from django.shortcuts import render
from .models import Referenz from .models import Referenz
from abschnitte.utils import render_textabschnitte from abschnitte.utils import render_textabschnitte
from django.shortcuts import get_object_or_404
# Create your views here. # Create your views here.
def tree(request): def tree(request):
@@ -9,7 +10,7 @@ def tree(request):
def detail(request, refid): def detail(request, refid):
referenz_item = Referenz.objects.get(id=refid) referenz_item = get_object_or_404(Referenz, id=refid)
referenz_item.erklaerung = render_textabschnitte(referenz_item.referenzerklaerung_set.order_by("order")) referenz_item.erklaerung = render_textabschnitte(referenz_item.referenzerklaerung_set.order_by("order"))
referenz_item.children = list(referenz_item.get_descendants(include_self=True)) referenz_item.children = list(referenz_item.get_descendants(include_self=True))
for child in referenz_item.children: for child in referenz_item.children:

View File

@@ -1,37 +1,44 @@
appdirs==1.4.4 appdirs==1.4.4
asgiref==3.8.1 asgiref==3.11.0
blessed==1.21.0 bleach==6.3.0
certifi==2025.8.3 blessed==1.27.0
charset-normalizer==3.4.3 certifi==2026.1.4
charset-normalizer==3.4.4
coverage==7.13.1
curtsies==0.4.3 curtsies==0.4.3
cwcwidth==0.1.10 cwcwidth==0.1.12
Django==5.2.9 Django==6.0.1
django-admin-sortable2==2.2.8 django-admin-sortable2==2.3
django-js-asset==3.1.2 django-js-asset==3.1.2
django-mptt==0.17.0 django-mptt==0.18.0
django-mptt-admin==2.8.0 django-mptt-admin==2.9.0
django-nested-admin==4.1.1 django-nested-admin==4.1.6
django-nested-inline==0.4.6 django-nested-inline==0.4.6
django-revproxy==0.13.0 django-revproxy==0.13.0
greenlet==3.2.4 greenlet==3.3.0
gunicorn==23.0.0 gunicorn==23.0.0
idna==3.10 idna==3.11
jedi==0.19.2 jedi==0.19.2
Markdown==3.8.2 jproperties==2.1.2
Markdown==3.10
packaging==25.0 packaging==25.0
parsedatetime==2.6 parsedatetime==2.6
parso==0.8.4 parso==0.8.5
pep8==1.7.1 pep8==1.7.1
prompt_toolkit==3.0.51 prompt_toolkit==3.0.52
pyfakefs==5.9.3
Pygments==2.19.2 Pygments==2.19.2
pysonar==1.2.1.3951
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
python-monkey-business==1.1.0 python-monkey-business==1.1.0
pyxdg==0.28 pyxdg==0.28
PyYAML==6.0.3
requests==2.32.5 requests==2.32.5
responses==0.25.8
six==1.17.0 six==1.17.0
sqlparse==0.5.3 sqlparse==0.5.5
tomli==2.2.1
urllib3==2.6.3 urllib3==2.6.3
wcwidth==0.2.13 wcwidth==0.2.14
bleach==6.1.0 webencodings==0.5.1
coverage==7.6.1 whitenoise==6.11.0
whitenoise==6.8.2

View File

@@ -0,0 +1,17 @@
# Generated by Django 6.0.1 on 2026-01-20 08:57
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('rollen', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='rolle',
options={'verbose_name': 'Rolle (für Relevanz)', 'verbose_name_plural': 'Rolleni (für Relevanz)'},
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 6.0.1 on 2026-02-12 13:27
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('rollen', '0002_alter_rolle_options'),
]
operations = [
migrations.AlterModelOptions(
name='rolle',
options={'verbose_name': 'Rolle', 'verbose_name_plural': 'Rollen'},
),
]

View File

@@ -9,9 +9,10 @@ class Rolle(models.Model):
return self.name return self.name
class Meta: class Meta:
verbose_name_plural="Rollen" verbose_name_plural="Rollen"
verbose_name="Rolle"
class RollenBeschreibung(Textabschnitt): class RollenBeschreibung(Textabschnitt):
abschnitt=models.ForeignKey(Rolle,on_delete=models.CASCADE) abschnitt=models.ForeignKey(Rolle,on_delete=models.CASCADE)
class Meta: class Meta:
verbose_name_plural="Rollenbeschreibung" verbose_name_plural="Rollenbeschreibung"
verbose_name="Rollenbeschreibungs-Abschnitt" verbose_name="Rollenbeschreibungs-Abschnitt"

View File

@@ -1,201 +0,0 @@
#!/bin/bash
# deploy-django-secret.sh
# Script to generate a secure Django SECRET_KEY and deploy it to Kubernetes
set -euo pipefail
# Configuration
NAMESPACE="${NAMESPACE:-vorgabenui}"
SECRET_NAME="vorgabenui-secrets"
SECRET_KEY_NAME="vorgabenui_secret"
K8S_DIR="$(dirname "$0")/../k8s"
SECRET_YAML="$K8S_DIR/django-secret.yaml"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Function to generate a secure Django SECRET_KEY
generate_secret_key() {
# Generate a 50-character secret key using Python (same as Django's default)
python3 -c "
import secrets
import string
# Django-style secret key generation
chars = string.ascii_letters + string.digits + '!@#$%^&*(-_=+)'
print(''.join(secrets.choice(chars) for _ in range(50)))
"
}
# Function to check if kubectl is available
check_kubectl() {
if ! command -v kubectl &> /dev/null; then
log_error "kubectl is not installed or not in PATH"
exit 1
fi
}
# Function to check if Python3 is available
check_python() {
if ! command -v python3 &> /dev/null; then
log_error "python3 is not installed or not in PATH"
exit 1
fi
}
# Function to create the secret
create_secret() {
local secret_key="$1"
local encoded_key
# Base64 encode the secret key
encoded_key=$(echo -n "$secret_key" | base64 -w 0)
log_info "Creating Kubernetes secret '$SECRET_NAME' in namespace '$NAMESPACE'..."
# Create the secret directly with kubectl
kubectl create secret generic "$SECRET_NAME" \
--from-literal="$SECRET_KEY_NAME=$secret_key" \
--namespace="$NAMESPACE" \
--dry-run=client -o yaml | kubectl apply -f -
if [ $? -eq 0 ]; then
log_info "Successfully created/updated secret '$SECRET_NAME'"
else
log_error "Failed to create/update secret '$SECRET_NAME'"
exit 1
fi
}
# Function to verify the secret
verify_secret() {
log_info "Verifying secret deployment..."
if kubectl get secret "$SECRET_NAME" --namespace="$NAMESPACE" &> /dev/null; then
log_info "Secret '$SECRET_NAME' exists in namespace '$NAMESPACE'"
# Show secret (without revealing the actual key)
kubectl describe secret "$SECRET_NAME" --namespace="$NAMESPACE"
return 0
else
log_error "Secret '$SECRET_NAME' not found in namespace '$NAMESPACE'"
return 1
fi
}
# Function to show usage
show_usage() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -n, --namespace NAMESPACE Kubernetes namespace (default: vorgabenui)"
echo " -s, --secret-name NAME Secret name (default: django-secrets)"
echo " -k, --key-name NAME Secret key name (default: django-secret-key)"
echo " -h, --help Show this help message"
echo ""
echo "Environment variables:"
echo " NAMESPACE Override default namespace"
echo ""
echo "Examples:"
echo " $0 # Deploy to vorgabenui namespace"
echo " $0 -n production # Deploy to production namespace"
echo " NAMESPACE=staging $0 # Deploy to staging namespace"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-n|--namespace)
NAMESPACE="$2"
shift 2
;;
-s|--secret-name)
SECRET_NAME="$2"
shift 2
;;
-k|--key-name)
SECRET_KEY_NAME="$2"
shift 2
;;
-h|--help)
show_usage
exit 0
;;
*)
log_error "Unknown option: $1"
show_usage
exit 1
;;
esac
done
# Main execution
main() {
log_info "Django SECRET_KEY Deployment Script"
log_info "==================================="
log_info "Namespace: $NAMESPACE"
log_info "Secret Name: $SECRET_NAME"
log_info "Secret Key Name: $SECRET_KEY_NAME"
echo ""
# Perform checks
check_kubectl
check_python
# Generate new secret key
log_info "Generating new Django SECRET_KEY..."
SECRET_KEY=$(generate_secret_key)
if [ -z "$SECRET_KEY" ]; then
log_error "Failed to generate secret key"
exit 1
fi
log_info "Generated secret key (first 10 chars): ${SECRET_KEY:0:10}..."
# Create namespace if it doesn't exist
if ! kubectl get namespace "$NAMESPACE" &> /dev/null; then
log_warn "Namespace '$NAMESPACE' does not exist, creating..."
kubectl create namespace "$NAMESPACE"
fi
# Create the secret
create_secret "$SECRET_KEY"
# Verify deployment
verify_secret
echo ""
log_info "Deployment completed successfully!"
log_info "To use this secret in your Django deployment, add the following to your pod spec:"
echo ""
echo " env:"
echo " - name: VORGABENUI_SECRET"
echo " valueFrom:"
echo " secretKeyRef:"
echo " name: $SECRET_NAME"
echo " key: $SECRET_KEY_NAME"
echo ""
log_warn "The old secret key in settings.py has been replaced with environment variable lookup."
log_warn "Make sure your Django deployment uses the environment variable before deploying."
}
# Run main function
main

View File

@@ -3,7 +3,7 @@
NAMESPACE="vorgabenui" NAMESPACE="vorgabenui"
SECRET_NAME="django-secret" SECRET_NAME="django-secret"
SECRET_FILE="argocd/secret.yaml" SECRET_FILE="templates/secret.yaml"
# Check if secret file exists # Check if secret file exists
if [ ! -f "$SECRET_FILE" ]; then if [ ! -f "$SECRET_FILE" ]; then

View File

@@ -36,10 +36,15 @@ NEW_MAIN_VERSION=$(echo "$MAIN_VERSION + 0.001" | bc | sed 's/^\./0./')
sed -i "s|image: git.baumann.gr/adebaumann/labhelper-data-loader:$LOADER_VERSION|image: git.baumann.gr/adebaumann/labhelper-data-loader:$NEW_LOADER_VERSION|" "$DEPLOYMENT_FILE" sed -i "s|image: git.baumann.gr/adebaumann/labhelper-data-loader:$LOADER_VERSION|image: git.baumann.gr/adebaumann/labhelper-data-loader:$NEW_LOADER_VERSION|" "$DEPLOYMENT_FILE"
sed -i "s|image: git.baumann.gr/adebaumann/labhelper:$MAIN_VERSION|image: git.baumann.gr/adebaumann/labhelper:$NEW_MAIN_VERSION|" "$DEPLOYMENT_FILE" sed -i "s|image: git.baumann.gr/adebaumann/labhelper:$MAIN_VERSION|image: git.baumann.gr/adebaumann/labhelper:$NEW_MAIN_VERSION|" "$DEPLOYMENT_FILE"
# Update the configmap version to match the main container
CONFIGMAP_FILE="argocd/configmap.yaml"
sed -i "s|VERSION: \"$MAIN_VERSION\"|VERSION: \"$NEW_MAIN_VERSION\"|" "$CONFIGMAP_FILE"
# Copy database # Copy database
cp "$DB_SOURCE" "$DB_DEST" cp "$DB_SOURCE" "$DB_DEST"
echo "Full deployment prepared:" echo "Full deployment prepared:"
echo " Data loader: $LOADER_VERSION -> $NEW_LOADER_VERSION" echo " Data loader: $LOADER_VERSION -> $NEW_LOADER_VERSION"
echo " Main container: $MAIN_VERSION -> $NEW_MAIN_VERSION" echo " Main container: $MAIN_VERSION -> $NEW_MAIN_VERSION"
echo " ConfigMap VERSION: $NEW_MAIN_VERSION"
echo " Database copied to $DB_DEST" echo " Database copied to $DB_DEST"

View File

@@ -23,5 +23,10 @@ NEW_VERSION=$(echo "$CURRENT_VERSION + 0.001" | bc | sed 's/^\./0./')
# Update the deployment file (only the main container, not the data-loader) # Update the deployment file (only the main container, not the data-loader)
sed -i "s|image: git.baumann.gr/adebaumann/vui:$CURRENT_VERSION|image: git.baumann.gr/adebaumann/vui:$NEW_VERSION|" "$DEPLOYMENT_FILE" sed -i "s|image: git.baumann.gr/adebaumann/vui:$CURRENT_VERSION|image: git.baumann.gr/adebaumann/vui:$NEW_VERSION|" "$DEPLOYMENT_FILE"
# Update the configmap version to match the main container
CONFIGMAP_FILE="argocd/configmap.yaml"
sed -i "s|VERSION: \"$CURRENT_VERSION\"|VERSION: \"$NEW_VERSION\"|" "$CONFIGMAP_FILE"
echo "Partial deployment prepared:" echo "Partial deployment prepared:"
echo " Main container: $CURRENT_VERSION -> $NEW_VERSION" echo " Main container: $CURRENT_VERSION -> $NEW_VERSION"
echo " ConfigMap VERSION: $NEW_VERSION"

View File

@@ -0,0 +1,17 @@
# Generated by Django 6.0.1 on 2026-01-20 08:57
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('stichworte', '0003_alter_stichworterklaerung_options'),
]
operations = [
migrations.AlterModelOptions(
name='stichwort',
options={'verbose_name': 'Stichwort', 'verbose_name_plural': 'Stichworte'},
),
]

View File

@@ -9,6 +9,7 @@ class Stichwort(models.Model):
class Meta: class Meta:
verbose_name_plural="Stichworte" verbose_name_plural="Stichworte"
verbose_name = "Stichwort"
class Stichworterklaerung (Textabschnitt): class Stichworterklaerung (Textabschnitt):
erklaerung = models.ForeignKey(Stichwort,on_delete=models.CASCADE) erklaerung = models.ForeignKey(Stichwort,on_delete=models.CASCADE)