Compare commits
35 Commits
29c1ad1dcf
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
|
67a967da67
|
|||
|
6af0b02442
|
|||
|
f7a20648b2
|
|||
|
bae8c71028
|
|||
|
a0495fdea0
|
|||
|
c125238f12
|
|||
| f5c0d9beac | |||
|
70752a2482
|
|||
|
633ecabdb9
|
|||
|
27e78d6f39
|
|||
|
ad9d55e986
|
|||
|
996ea628c7
|
|||
|
523b991493
|
|||
|
310c4fdd0b
|
|||
| 353a8a5697 | |||
| f1d3c88a45 | |||
| 996584ef68 | |||
| 18fac6e8b9 | |||
| 492f8b4e90 | |||
| e86e3c19b5 | |||
| 938424a02e | |||
| b9e1a06e09 | |||
| 1a0c74bfa2 | |||
| 82455358ff | |||
| 713798352d | |||
| 0e8e2da169 | |||
| e8f34f7fa5 | |||
| 67d4087e3a | |||
| ffda7ca601 | |||
| 9d0a838238 | |||
| b0725fb385 | |||
| c77e8c0432 | |||
| 51969141e7 | |||
| b7f50ce30f | |||
| d3d0298ad1 |
22
.argocdignore
Normal file
22
.argocdignore
Normal file
@@ -0,0 +1,22 @@
|
||||
# ArgoCD ignore patterns
|
||||
# Exclude template files from ArgoCD deployment
|
||||
|
||||
# Secret templates (deployed separately by scripts)
|
||||
templates/
|
||||
**/secret.yaml
|
||||
|
||||
# Documentation and scripts
|
||||
docs/
|
||||
scripts/
|
||||
*.md
|
||||
README*
|
||||
|
||||
# Development files
|
||||
.env*
|
||||
.git*
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# CI/CD files
|
||||
.gitea/
|
||||
.github/
|
||||
@@ -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
3
.gitignore
vendored
@@ -16,4 +16,5 @@ AGENT*.md
|
||||
# Diagram cache directory
|
||||
media/diagram_cache/
|
||||
.env
|
||||
data/db.sqlite3
|
||||
data/
|
||||
dataremote/
|
||||
|
||||
20
Dockerfile
20
Dockerfile
@@ -1,18 +1,22 @@
|
||||
FROM python:3.14 AS baustelle
|
||||
FROM python:3.15.0a5-trixie AS baustelle
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
RUN pip install --upgrade pip
|
||||
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.14-slim
|
||||
RUN useradd -m -r appuser && \
|
||||
FROM python:3.15.0a5-slim-trixie
|
||||
RUN useradd -m -r -u 99 appuser && \
|
||||
mkdir /app && \
|
||||
chown -R appuser /app
|
||||
|
||||
COPY --from=baustelle /usr/local/lib/python3.14/site-packages/ /usr/local/lib/python3.14/site-packages/
|
||||
COPY --from=baustelle /usr/local/lib/python3.15/site-packages/ /usr/local/lib/python3.15/site-packages/
|
||||
COPY --from=baustelle /usr/local/bin/ /usr/local/bin/
|
||||
RUN rm /usr/bin/tar /usr/lib/x86_64-linux-gnu/libncur*
|
||||
WORKDIR /app
|
||||
@@ -21,6 +25,8 @@ ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
USER appuser
|
||||
EXPOSE 8000
|
||||
# Set build environment variable to enable fallback secret key during build
|
||||
ENV DOCKER_BUILDKIT=1
|
||||
RUN rm -rvf /app/Dockerfile* \
|
||||
/app/README.md \
|
||||
/app/argocd \
|
||||
@@ -30,7 +36,11 @@ RUN rm -rvf /app/Dockerfile* \
|
||||
/app/requirements.txt \
|
||||
/app/node_modules \
|
||||
/app/*.json \
|
||||
/app/scripts \
|
||||
/app/test_*.py && \
|
||||
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"]
|
||||
|
||||
|
||||
@@ -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
|
||||
- **Namespace**: vorgabenui
|
||||
|
||||
#### `configmap.yaml`
|
||||
- **Purpose**: Django application configuration
|
||||
- **Contains**: Environment variables, application settings, version information
|
||||
- **Namespace**: vorgabenui
|
||||
- **Version**: 0.990
|
||||
|
||||
#### `deployment.yaml`
|
||||
- **Purpose**: Main application deployment configuration
|
||||
- **Contains**: Django application container, environment variables, resource limits
|
||||
- **Replicas**: Configurable replica count for high availability
|
||||
- **Version**: 0.990
|
||||
|
||||
#### `ingress.yaml`
|
||||
- **Purpose**: External access configuration
|
||||
- **Host**: Configurable hostname for the application
|
||||
- **TLS**: SSL/TLS termination configuration
|
||||
- **Backend**: Routes traffic to the Django application service
|
||||
- **Ingress Class**: traefik
|
||||
|
||||
#### `nfs-pv.yaml`
|
||||
- **Purpose**: PersistentVolume definition for NFS storage
|
||||
- **Server**: 192.168.17.199
|
||||
- **Path**: /mnt/user/vorgabenui
|
||||
- **Path**: /mnt/user/kubernetesdata/vorgabenui
|
||||
- **Access**: ReadWriteMany for multi-pod access
|
||||
- **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`
|
||||
- **Purpose**: Deployment configuration for the diagram generation service
|
||||
- **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
|
||||
1. NFS server must be running at 192.168.17.199
|
||||
2. The directory `/mnt/user/vorgabenui` must exist and be exported
|
||||
3. Kubernetes nodes must have NFS client utilities installed
|
||||
4. For MicroK8s: `microk8s enable nfs`
|
||||
#### `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
|
||||
|
||||
## MicroK8s Addons Required
|
||||
|
||||
@@ -136,7 +151,7 @@ microk8s kubectl get pods -n ingress
|
||||
microk8s kubectl get svc -n ingress
|
||||
|
||||
# Test ingress connectivity
|
||||
curl -k https://your-domain.com
|
||||
curl -k https://vorgabenportal.knowyoursecurity.com
|
||||
```
|
||||
|
||||
#### Storage Issues
|
||||
@@ -159,24 +174,143 @@ On the NFS server (192.168.17.199), ensure the following:
|
||||
|
||||
```bash
|
||||
# Create the shared directory
|
||||
sudo mkdir -p /mnt/user/vorgabenui
|
||||
sudo chmod 755 /mnt/user/vorgabenui
|
||||
sudo mkdir -p /mnt/user/kubernetesdata/vorgabenui
|
||||
sudo chmod 755 /mnt/user/kubernetesdata/vorgabenui
|
||||
|
||||
# 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
|
||||
sudo exportfs -a
|
||||
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
|
||||
|
||||
1. **StorageClass** (`nfs-storageclass.yaml`) - Defines NFS storage class
|
||||
2. **PersistentVolume** (`nfs-pv.yaml`) - Creates the NFS volume
|
||||
3. **PersistentVolumeClaim** (`001_pvc.yaml`) - Claims storage for application
|
||||
4. **Application Deployments** (`deployment.yaml`, `diagrammer.yaml`) - Deploy application services
|
||||
5. **Ingress** (`ingress.yaml`) - Configure external access
|
||||
4. **ConfigMap** (`configmap.yaml`) - Deploy Django configuration
|
||||
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
|
||||
|
||||
@@ -227,7 +361,7 @@ kubectl describe pod <pod-name> -n vorgabenui
|
||||
## Maintenance
|
||||
|
||||
### 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
|
||||
|
||||
### Monitoring
|
||||
|
||||
@@ -49,7 +49,7 @@ python manage.py import-document Documentation/import\ formats/r009.txt \
|
||||
|
||||
### Dry-Run Modus
|
||||
|
||||
Der Dry-Run Modus ist besonders nützlich zum Testen:
|
||||
Der Dry-Run Modus ist zum Testen gedacht:
|
||||
|
||||
```bash
|
||||
python manage.py import-document r009.txt \
|
||||
@@ -73,7 +73,7 @@ python manage.py import-document r009.txt \
|
||||
--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
|
||||
|
||||
@@ -257,7 +257,7 @@ Abschliessender Text nach der Liste.
|
||||
>>>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
|
||||
- Technik
|
||||
- Informationen
|
||||
@@ -275,11 +275,7 @@ Das Thema muss in der Datenbank bereits als `Thema`-Objekt existieren. Übliche
|
||||
|
||||
oder inline:
|
||||
|
||||
```
|
||||
>>>Nummer: 1
|
||||
```
|
||||
|
||||
Die Nummer wird als Integer gespeichert. Sie ist eindeutig innerhalb eines Dokuments und Themas.
|
||||
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).
|
||||
|
||||
#### Titel (Pflicht)
|
||||
|
||||
@@ -322,14 +318,6 @@ Komma-getrennte Liste:
|
||||
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)
|
||||
@@ -347,7 +335,7 @@ Jede Zeile wird als separate Checklistenfrage gespeichert.
|
||||
|
||||
### 1. Dry-Run vor Import
|
||||
|
||||
Führen Sie immer zuerst einen Dry-Run durch:
|
||||
Immer zuerst einen Dry-Run durchführen:
|
||||
|
||||
```bash
|
||||
python manage.py import-document datei.txt \
|
||||
@@ -359,7 +347,7 @@ python manage.py import-document datei.txt \
|
||||
|
||||
### 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 manage.py shell
|
||||
@@ -391,11 +379,11 @@ Folgende Abschnitttypen müssen in der Datenbank existieren:
|
||||
- `code`
|
||||
- `diagramm`
|
||||
|
||||
Prüfen Sie diese in der Autorenumgebung unter "Abschnitttypen".
|
||||
Prüfen in der Autorenumgebung unter "Abschnitttypen".
|
||||
|
||||
### 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
|
||||
|
||||
@@ -436,37 +424,37 @@ Leerzeilen innerhalb eines Abschnitts werden beibehalten. Eine Leerzeile nach ei
|
||||
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
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`
|
||||
- Schreibweise prüfen (Gross-/Kleinschreibung und "-"/" " wird normalisiert)
|
||||
- Wenn nötig Abschnitttyp in der Autorenumgebung erstellen (Achtung! Ausgabeformat muss im Code definiert werden)
|
||||
- Standardtypen: `text`, `liste geordnet`, `liste ungeordnet`, `tabelle`, `code`
|
||||
|
||||
### Vorgabe wird nicht importiert
|
||||
|
||||
Prüfen Sie:
|
||||
Prüfen:
|
||||
- Ist `>>>Nummer` gesetzt?
|
||||
- Ist `>>>Titel` gesetzt?
|
||||
- Existiert das Thema?
|
||||
|
||||
Verwenden Sie `--dry-run --verbose` für detaillierte Informationen.
|
||||
`--dry-run --verbose` für detaillierte Informationen.
|
||||
|
||||
## Weitere Informationen
|
||||
|
||||
### Beispieldateien
|
||||
|
||||
Beispieldateien finden Sie in:
|
||||
Beispieldateien:
|
||||
- `Documentation/import formats/r009.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
|
||||
- `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.
|
||||
|
||||
5
VorgabenUI/context_processors.py
Normal file
5
VorgabenUI/context_processors.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def version(request):
|
||||
return {'version': settings.VERSION}
|
||||
@@ -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
|
||||
@@ -20,13 +20,37 @@ 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 = '429ti9tugj9güLLO))(G&G94KF452R3Fieaek$&6s#zlao-ca!#)_@j6*u+8s&bvfil^qyo%&-sov$ysi'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
DEBUG = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 'yes', 'on')
|
||||
|
||||
ALLOWED_HOSTS = ["10.128.128.144","localhost","127.0.0.1","*"]
|
||||
# Application version (from ConfigMap)
|
||||
VERSION = os.environ.get('VERSION', '0.0.0')
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = os.environ.get('VORGABENUI_SECRET')
|
||||
if not SECRET_KEY:
|
||||
# Check if we're in a build environment (Docker build, CI, etc.)
|
||||
is_build_env = any([
|
||||
os.environ.get('DOCKER_BUILDKIT'), # Docker build
|
||||
os.environ.get('CI'), # CI environment
|
||||
os.environ.get('GITHUB_ACTIONS'), # GitHub Actions
|
||||
os.environ.get('GITEA_ACTIONS'), # Gitea Actions
|
||||
])
|
||||
|
||||
# Use DEBUG environment variable or assume debug mode for local development
|
||||
debug_mode = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 'yes', 'on')
|
||||
|
||||
if debug_mode or is_build_env:
|
||||
# Fixed fallback key for local development and build environments
|
||||
SECRET_KEY = 'dev-fallback-key-for-local-debugging-only-not-for-production-use-12345'
|
||||
if not is_build_env: # Don't log during build to avoid noise
|
||||
import logging
|
||||
logging.warning("🚨 Using fallback SECRET_KEY for local development. This should NEVER happen in production!")
|
||||
else:
|
||||
raise ValueError("VORGABENUI_SECRET environment variable is required")
|
||||
|
||||
|
||||
ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', "10.128.128.144,localhost,127.0.0.1,*").split(",")
|
||||
|
||||
# Application definition
|
||||
|
||||
@@ -37,6 +61,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'whitenoise',
|
||||
'dokumente',
|
||||
'abschnitte',
|
||||
'stichworte',
|
||||
@@ -48,6 +73,7 @@ INSTALLED_APPS = [
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
@@ -71,6 +97,7 @@ TEMPLATES = [
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'VorgabenUI.context_processors.version',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -115,7 +142,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
|
||||
LANGUAGE_CODE = 'de-ch'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = 'Europe/Zurich'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@@ -143,6 +170,12 @@ DIAGRAM_CACHE_DIR = 'diagram_cache' # relative to MEDIA_ROOT
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Custom error pages
|
||||
handler400 = 'pages.views.custom_400'
|
||||
handler403 = 'pages.views.custom_403'
|
||||
handler404 = 'pages.views.custom_404'
|
||||
handler500 = 'pages.views.custom_500'
|
||||
DATA_UPLOAD_MAX_NUMBER_FIELDS=10250
|
||||
NESTED_ADMIN_LAZY_INLINES = True
|
||||
|
||||
|
||||
@@ -40,9 +40,13 @@ urlpatterns = [
|
||||
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(template_name='registration/password_change_done.html'), name='password_change_done'),
|
||||
]
|
||||
|
||||
# Serve static files
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
# 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:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
28
argocd/configmap.yaml
Normal file
28
argocd/configmap.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
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"
|
||||
@@ -14,19 +14,51 @@ spec:
|
||||
app: django
|
||||
spec:
|
||||
securityContext:
|
||||
fsGroup: 999
|
||||
fsGroup: 99
|
||||
fsGroupChangePolicy: "OnRootMismatch"
|
||||
initContainers:
|
||||
- name: loader
|
||||
image: git.baumann.gr/adebaumann/vui-data-loader:0.11
|
||||
command: [ "sh","-c","cp -n preload/preload.sqlite3 /data/db.sqlite3; chown -R 999:999 /data; ls -la /data; sleep 10; exit 0" ]
|
||||
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" ]
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /data
|
||||
containers:
|
||||
- name: web
|
||||
image: git.baumann.gr/adebaumann/vui:0.973
|
||||
image: git.baumann.gr/adebaumann/vui:0.990
|
||||
imagePullPolicy: Always
|
||||
securityContext:
|
||||
runAsUser: 99
|
||||
env:
|
||||
# Secret configuration
|
||||
- name: VORGABENUI_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: vorgabenui-secrets
|
||||
key: vorgabenui_secret
|
||||
# ConfigMap configuration
|
||||
- name: DEBUG
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: django-config
|
||||
key: DEBUG
|
||||
- name: DJANGO_ALLOWED_HOSTS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: django-config
|
||||
key: DJANGO_ALLOWED_HOSTS
|
||||
- name: DJANGO_SETTINGS_MODULE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: django-config
|
||||
key: DJANGO_SETTINGS_MODULE
|
||||
- name: VERSION
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: django-config
|
||||
key: VERSION
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
volumeMounts:
|
||||
|
||||
@@ -4,6 +4,9 @@ metadata:
|
||||
name: django-data-pv
|
||||
namespace: vorgabenui
|
||||
spec:
|
||||
claimRef:
|
||||
name: django-data-pvc
|
||||
namespace: vorgabenui
|
||||
capacity:
|
||||
storage: 2Gi
|
||||
accessModes:
|
||||
@@ -12,4 +15,4 @@ spec:
|
||||
storageClassName: nfs
|
||||
nfs:
|
||||
server: 192.168.17.199
|
||||
path: /mnt/user/vorgabenui
|
||||
path: /mnt/user/kubernetesdata/vorgabenui
|
||||
|
||||
512
code-review.md
Normal file
512
code-review.md
Normal 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*
|
||||
383
docs/kubernetes-secrets.md
Normal file
383
docs/kubernetes-secrets.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# Kubernetes Configuration Management for VorgabenUI Django
|
||||
|
||||
This document describes how to manage Django configuration using Kubernetes secrets and ConfigMaps.
|
||||
|
||||
## Overview
|
||||
|
||||
Django configuration has been moved to Kubernetes-native resources for improved security and flexibility:
|
||||
|
||||
### **Secrets** (for sensitive data)
|
||||
- `VORGABENUI_SECRET` - Django SECRET_KEY
|
||||
- Future: Database passwords, API keys, etc.
|
||||
|
||||
### **ConfigMaps** (for non-sensitive configuration)
|
||||
- `DEBUG` - Debug mode setting
|
||||
- `DJANGO_ALLOWED_HOSTS` - Allowed hostnames
|
||||
- `DJANGO_SETTINGS_MODULE` - Settings module path
|
||||
- Application configuration settings
|
||||
|
||||
This approach ensures that:
|
||||
|
||||
1. Sensitive data is not stored in version control
|
||||
2. Configuration is environment-specific
|
||||
3. Non-sensitive settings are easily manageable
|
||||
4. Follows Kubernetes best practices
|
||||
5. Includes fallback for local development
|
||||
|
||||
## Files Changed
|
||||
|
||||
### VorgabenUI/settings.py
|
||||
- Replaced hardcoded `SECRET_KEY` with `VORGABENUI_SECRET` environment variable lookup
|
||||
- Added fallback secret key for local development (only works when DEBUG=True)
|
||||
- Added warning when fallback key is used
|
||||
|
||||
### Files Created/Updated
|
||||
|
||||
#### **Configuration Resources**
|
||||
- `argocd/configmap.yaml` - Django configuration (DEBUG, ALLOWED_HOSTS, etc.)
|
||||
- `templates/configmap.yaml` - ConfigMap template (excluded from ArgoCD)
|
||||
- `templates/secret.yaml` - Secret template (excluded from ArgoCD deployment)
|
||||
- `argocd/secret.yaml` - ArgoCD-specific secret template with ignore annotation
|
||||
|
||||
#### **Deployment Configuration**
|
||||
- `argocd/deployment.yaml` - Updated with Secret and ConfigMap environment variables
|
||||
- `.argocdignore` - ArgoCD ignore patterns for templates and scripts
|
||||
|
||||
#### **Deployment Scripts**
|
||||
- `scripts/deploy-argocd-secret.sh` - ArgoCD-specific script to deploy secrets
|
||||
- `scripts/deploy-argocd-configmap.sh` - ArgoCD-specific script to deploy ConfigMap
|
||||
|
||||
#### **Application Code**
|
||||
- `VorgabenUI/settings.py` - Updated to use environment variables from ConfigMap
|
||||
|
||||
#### **Examples and Documentation**
|
||||
- `k8s/django-secret.yaml` - Updated for consistency (vorgabenui namespace)
|
||||
- `k8s/django-deployment-example.yaml` - Updated example deployment
|
||||
- `scripts/deploy-django-secret.sh` - Updated with new defaults
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Deploy ConfigMap (ArgoCD Production)
|
||||
|
||||
**Deploy configuration first** (required before the application starts):
|
||||
|
||||
```bash
|
||||
# Deploy ConfigMap to vorgabenui namespace
|
||||
./scripts/deploy-argocd-configmap.sh
|
||||
|
||||
# Verify existing ConfigMap
|
||||
./scripts/deploy-argocd-configmap.sh --verify-only
|
||||
|
||||
# Dry run to see what would happen
|
||||
./scripts/deploy-argocd-configmap.sh --dry-run
|
||||
|
||||
# Get help
|
||||
./scripts/deploy-argocd-configmap.sh --help
|
||||
```
|
||||
|
||||
### 2. Deploy the Secret (ArgoCD Production)
|
||||
|
||||
**Deploy secret second** (contains sensitive SECRET_KEY):
|
||||
|
||||
```bash
|
||||
# Deploy secret to vorgabenui namespace
|
||||
./scripts/deploy-argocd-secret.sh
|
||||
|
||||
# Verify existing secret
|
||||
./scripts/deploy-argocd-secret.sh --verify-only
|
||||
|
||||
# Dry run to see what would happen
|
||||
./scripts/deploy-argocd-secret.sh --dry-run
|
||||
|
||||
# Get help
|
||||
./scripts/deploy-argocd-secret.sh --help
|
||||
```
|
||||
|
||||
### 3. Deploy Resources for Other Environments
|
||||
|
||||
For development or other environments, use the general scripts:
|
||||
|
||||
```bash
|
||||
# Deploy ConfigMap to vorgabenui namespace (default)
|
||||
./scripts/deploy-django-secret.sh # (includes ConfigMap deployment)
|
||||
|
||||
# Deploy to specific namespace
|
||||
./scripts/deploy-django-secret.sh -n development
|
||||
|
||||
# Get help
|
||||
./scripts/deploy-django-secret.sh --help
|
||||
```
|
||||
|
||||
### 4. Environment Variable Configuration
|
||||
|
||||
The ArgoCD deployment (`argocd/deployment.yaml`) is configured with:
|
||||
|
||||
**Secret Variables:**
|
||||
```yaml
|
||||
env:
|
||||
# Secret configuration
|
||||
- name: VORGABENUI_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: vorgabenui-secrets
|
||||
key: vorgabenui_secret
|
||||
```
|
||||
|
||||
**ConfigMap Variables:**
|
||||
```yaml
|
||||
# ConfigMap configuration
|
||||
- name: DEBUG
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: django-config
|
||||
key: DEBUG
|
||||
- name: DJANGO_ALLOWED_HOSTS
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: django-config
|
||||
key: DJANGO_ALLOWED_HOSTS
|
||||
- name: DJANGO_SETTINGS_MODULE
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
name: django-config
|
||||
key: DJANGO_SETTINGS_MODULE
|
||||
```
|
||||
|
||||
For other deployments, see `k8s/django-deployment-example.yaml` for a complete example.
|
||||
|
||||
### 5. Verify the Deployment
|
||||
|
||||
**Check ConfigMap:**
|
||||
```bash
|
||||
kubectl get configmap django-config -n vorgabenui
|
||||
kubectl describe configmap django-config -n vorgabenui
|
||||
```
|
||||
|
||||
**Check Secret:**
|
||||
```bash
|
||||
kubectl get secrets vorgabenui-secrets -n vorgabenui
|
||||
kubectl describe secret vorgabenui-secrets -n vorgabenui
|
||||
```
|
||||
|
||||
**Check Django pods can access configuration:**
|
||||
```bash
|
||||
# Check secret variable
|
||||
kubectl exec -n vorgabenui deployment/django -- printenv VORGABENUI_SECRET
|
||||
|
||||
# Check ConfigMap variables
|
||||
kubectl exec -n vorgabenui deployment/django -- printenv DEBUG
|
||||
kubectl exec -n vorgabenui deployment/django -- printenv DJANGO_ALLOWED_HOSTS
|
||||
|
||||
# Check all environment variables
|
||||
kubectl exec -n vorgabenui deployment/django -- printenv | grep -E "(VORGABENUI|DEBUG|DJANGO)"
|
||||
```
|
||||
|
||||
## Development Environment
|
||||
|
||||
### Local Development with Fallback
|
||||
|
||||
The application now includes a fallback secret key for local development. When running locally:
|
||||
|
||||
1. **Automatic fallback**: If `VORGABENUI_SECRET` is not set and `DEBUG=True`, a fallback key is used automatically
|
||||
2. **Warning message**: The application will log a warning when using the fallback key (except during builds)
|
||||
3. **Production safety**: Fallback only works when `DEBUG=True` or in build environments
|
||||
|
||||
### Docker Build Support
|
||||
|
||||
The Django settings are designed to work seamlessly during Docker builds:
|
||||
|
||||
1. **Build environment detection**: Automatically detects Docker builds, CI environments
|
||||
2. **Fallback activation**: Uses fallback key during build without requiring environment variables
|
||||
3. **No build-time secrets**: No need to provide `VORGABENUI_SECRET` during `docker build`
|
||||
4. **Runtime security**: Production containers still require the proper environment variable
|
||||
|
||||
**Supported build environments:**
|
||||
- Docker builds (`DOCKER_BUILDKIT`)
|
||||
- CI environments (`CI`)
|
||||
- GitHub Actions (`GITHUB_ACTIONS`)
|
||||
- Gitea Actions (`GITEA_ACTIONS`)
|
||||
- Local development (`DEBUG=True`)
|
||||
|
||||
### Manual Environment Variable
|
||||
|
||||
You can still set the environment variable manually:
|
||||
|
||||
```bash
|
||||
# Option 1: Export the variable
|
||||
export VORGABENUI_SECRET="your-development-key-here"
|
||||
python manage.py runserver
|
||||
|
||||
# Option 2: Use a .env file (recommended)
|
||||
echo "VORGABENUI_SECRET=your-development-key-here" > .env
|
||||
# Then load it in your settings or use python-dotenv
|
||||
```
|
||||
|
||||
### Development vs Production
|
||||
|
||||
- **Local Development**: Fallback key works automatically when `DEBUG=True`
|
||||
- **Production**: Must have `VORGABENUI_SECRET` environment variable set, no fallback
|
||||
|
||||
## ArgoCD Integration and Exclusions
|
||||
|
||||
### Preventing ArgoCD from Deploying Secret Templates
|
||||
|
||||
This setup includes multiple approaches to prevent ArgoCD from trying to deploy the secret template:
|
||||
|
||||
#### 1. Template Directory (`templates/`)
|
||||
- Secret template moved to `templates/` directory
|
||||
- ArgoCD deployment script automatically uses this location
|
||||
- Excluded via `.argocdignore` file
|
||||
|
||||
#### 2. ArgoCD Ignore Annotation
|
||||
- `argocd/secret.yaml` has `argocd.argoproj.io/ignore: "true"` annotation
|
||||
- Provides fallback if templates directory approach fails
|
||||
|
||||
#### 3. `.argocdignore` File
|
||||
- Global exclusion patterns for templates, scripts, and documentation
|
||||
- Prevents ArgoCD from syncing non-deployment files
|
||||
|
||||
### ArgoCD Sync Behavior
|
||||
- ArgoCD will sync only the actual deployment files (`deployment.yaml`, `ingress.yaml`, etc.)
|
||||
- Secret templates are excluded and must be deployed manually using the deployment script
|
||||
- This ensures secrets are created outside of GitOps workflow for security
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Never commit the actual SECRET_KEY** - Only templates and scripts are in version control
|
||||
2. **Use different keys per environment** - Production, staging, and development should all have unique keys
|
||||
3. **Rotate keys regularly** - Run the deployment script periodically to generate new keys
|
||||
4. **Limit access** - Use Kubernetes RBAC to control who can access secrets
|
||||
5. **ArgoCD exclusion** - Secret templates are excluded from ArgoCD to prevent empty/template secrets from being deployed
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Django fails to start with "VORGABENUI_SECRET environment variable is required"
|
||||
|
||||
This means the environment variable is not set in your pod and fallback conditions aren't met. Check:
|
||||
|
||||
1. **Secret exists**: `kubectl get secret vorgabenui-secrets -n vorgabenui`
|
||||
2. **Deployment references secret correctly**: Check `argocd/deployment.yaml` env section
|
||||
3. **Pod has environment variable**: `kubectl exec <pod-name> -n vorgabenui -- env | grep VORGABENUI_SECRET`
|
||||
4. **For local development**: Ensure `DEBUG=True` to use the fallback key
|
||||
5. **For Docker builds**: Build should work automatically with fallback
|
||||
|
||||
### Docker build fails with SECRET_KEY error
|
||||
|
||||
This should no longer happen with the updated settings. If you still see issues:
|
||||
|
||||
1. **Check build environment variables**: Build should detect `DOCKER_BUILDKIT=1`
|
||||
2. **Verify settings changes**: Ensure the updated `settings.py` is being used
|
||||
3. **Force environment detection**: Set `CI=1` during build if needed
|
||||
4. **Use explicit DEBUG**: Set `DEBUG=True` during build as fallback
|
||||
|
||||
### Secret deployment fails
|
||||
|
||||
Check that:
|
||||
|
||||
1. You have kubectl access to the cluster
|
||||
2. You have permission to create secrets in the `vorgabenui` namespace
|
||||
3. Python3 is available for key generation
|
||||
4. The ArgoCD secret template exists: `argocd/secret.yaml`
|
||||
|
||||
### Key rotation
|
||||
|
||||
To rotate the SECRET_KEY:
|
||||
|
||||
1. **For ArgoCD production**: Run `./scripts/deploy-argocd-secret.sh` again
|
||||
2. **For other environments**: Run `./scripts/deploy-django-secret.sh` again
|
||||
3. Restart your Django pods to pick up the new key:
|
||||
```bash
|
||||
# For ArgoCD production
|
||||
kubectl rollout restart deployment/django -n vorgabenui
|
||||
|
||||
# For other environments
|
||||
kubectl rollout restart deployment/your-django-deployment -n your-namespace
|
||||
```
|
||||
|
||||
## Script Options
|
||||
|
||||
### ArgoCD Production Scripts
|
||||
|
||||
#### **ConfigMap Script (`deploy-argocd-configmap.sh`)**
|
||||
|
||||
Deploy Django configuration (non-sensitive):
|
||||
|
||||
- `--verify-only` - Only verify existing ConfigMap, don't deploy
|
||||
- `--dry-run` - Show what would be deployed without applying
|
||||
- `-h, --help` - Show help message
|
||||
|
||||
Configuration is hardcoded for ArgoCD:
|
||||
- Namespace: `vorgabenui`
|
||||
- ConfigMap name: `django-config`
|
||||
- ConfigMap file: `argocd/configmap.yaml`
|
||||
|
||||
#### **Secret Script (`deploy-argocd-secret.sh`)**
|
||||
|
||||
Deploy sensitive configuration:
|
||||
|
||||
- `--verify-only` - Only verify existing secret, don't create new one
|
||||
- `--dry-run` - Show what would be done without making changes
|
||||
- `-h, --help` - Show help message
|
||||
|
||||
Configuration is hardcoded for ArgoCD:
|
||||
- Namespace: `vorgabenui`
|
||||
- Secret name: `vorgabenui-secrets`
|
||||
- Secret key: `vorgabenui_secret`
|
||||
- Template location: `templates/secret.yaml` (excluded from ArgoCD)
|
||||
|
||||
### General Script (`deploy-django-secret.sh`)
|
||||
|
||||
For development and other environments:
|
||||
|
||||
- `-n, --namespace NAMESPACE` - Target Kubernetes namespace (default: vorgabenui)
|
||||
- `-s, --secret-name NAME` - Secret name (default: vorgabenui-secrets)
|
||||
- `-k, --key-name NAME` - Secret key name (default: vorgabenui_secret)
|
||||
- `-h, --help` - Show help message
|
||||
|
||||
Environment variables:
|
||||
- `NAMESPACE` - Override default namespace
|
||||
|
||||
## Migration from Hardcoded Key
|
||||
|
||||
### Migration from Old Setup
|
||||
|
||||
If you're migrating from the previous `DJANGO_SECRET_KEY` setup:
|
||||
|
||||
1. **Deploy the new secret** using `./scripts/deploy-argocd-secret.sh`
|
||||
2. **Update any existing deployments** to use `VORGABENUI_SECRET` instead of `DJANGO_SECRET_KEY`
|
||||
3. **Test locally** - the fallback key should work automatically in DEBUG mode
|
||||
4. **Deploy the updated application** - ArgoCD deployment is already configured
|
||||
|
||||
### Migration from Hardcoded Key
|
||||
|
||||
If you're migrating from a completely hardcoded key:
|
||||
|
||||
1. **Backup your current key** (in case you need to rollback)
|
||||
2. **Deploy the secret first** using the deployment script
|
||||
3. **Apply the updated ArgoCD deployment** (already done in this setup)
|
||||
4. **Test thoroughly** - local development should work with fallback
|
||||
5. **Deploy the updated settings.py** after confirming the secret works
|
||||
|
||||
The ArgoCD deployment (`argocd/deployment.yaml`) now includes the environment variable configuration, so Django will automatically pick up the secret after deployment.
|
||||
|
||||
## Deployment Order
|
||||
|
||||
**Critical: Deploy resources in this order:**
|
||||
|
||||
1. **ConfigMap first** (required for Django to start):
|
||||
```bash
|
||||
./scripts/deploy-argocd-configmap.sh
|
||||
```
|
||||
|
||||
2. **Secret second** (contains sensitive data):
|
||||
```bash
|
||||
./scripts/deploy-argocd-secret.sh
|
||||
```
|
||||
|
||||
3. **Application deployment** (ArgoCD will sync this automatically):
|
||||
```bash
|
||||
kubectl apply -f argocd/deployment.yaml
|
||||
# OR let ArgoCD sync from Git
|
||||
```
|
||||
|
||||
If you deploy in the wrong order, Django pods will fail to start because they require both the ConfigMap and Secret to be available.
|
||||
@@ -1,16 +1,10 @@
|
||||
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.utils.html import format_html
|
||||
from mptt.forms import TreeNodeMultipleChoiceField
|
||||
from mptt.admin import DraggableMPTTAdmin
|
||||
from adminsortable2.admin import SortableInlineAdminMixin, SortableAdminBase
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
# Register your models here.
|
||||
from nested_admin import NestedStackedInline, NestedModelAdmin, NestedTabularInline
|
||||
from .models import *
|
||||
from stichworte.models import Stichwort, Stichworterklaerung
|
||||
from referenzen.models import Referenz
|
||||
|
||||
|
||||
|
||||
@@ -69,7 +63,6 @@ class GeltungsbereichInline(NestedStackedInline):
|
||||
extra=0
|
||||
sortable_field_name = "order"
|
||||
show_change_link=True
|
||||
classes = ['collapse']
|
||||
verbose_name_plural = "Geltungsbereich-Abschnitte"
|
||||
fieldsets = (
|
||||
(None, {
|
||||
@@ -83,7 +76,6 @@ class EinleitungInline(NestedStackedInline):
|
||||
extra = 0
|
||||
sortable_field_name = "order"
|
||||
show_change_link = True
|
||||
classes = ['collapse']
|
||||
verbose_name_plural = "Einleitungs-Abschnitte"
|
||||
fieldsets = (
|
||||
(None, {
|
||||
@@ -93,8 +85,6 @@ class EinleitungInline(NestedStackedInline):
|
||||
)
|
||||
|
||||
class VorgabeForm(forms.ModelForm):
|
||||
referenzen = TreeNodeMultipleChoiceField(queryset=Referenz.objects.all(), required=False)
|
||||
|
||||
class Meta:
|
||||
model = Vorgabe
|
||||
fields = '__all__'
|
||||
@@ -106,7 +96,7 @@ class VorgabeForm(forms.ModelForm):
|
||||
raise forms.ValidationError('Thema ist ein Pflichtfeld. Bitte wählen Sie ein Thema aus.')
|
||||
return thema
|
||||
|
||||
class VorgabeInline(SortableInlineAdminMixin, NestedStackedInline):
|
||||
class VorgabeInline(NestedStackedInline):
|
||||
model = Vorgabe
|
||||
form = VorgabeForm
|
||||
extra = 0
|
||||
@@ -165,7 +155,7 @@ class StichwortAdmin(NestedModelAdmin):
|
||||
count = len(vorgaben_list)
|
||||
|
||||
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 += "<table style='width: 100%; border-collapse: collapse;'>"
|
||||
@@ -186,7 +176,7 @@ class StichwortAdmin(NestedModelAdmin):
|
||||
html += "</tbody></table>"
|
||||
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"
|
||||
|
||||
def get_queryset(self, request):
|
||||
@@ -204,7 +194,7 @@ class PersonAdmin(admin.ModelAdmin):
|
||||
|
||||
|
||||
@admin.register(Dokument)
|
||||
class DokumentAdmin(SortableAdminBase, NestedModelAdmin):
|
||||
class DokumentAdmin(NestedModelAdmin):
|
||||
actions_on_top=True
|
||||
inlines = [EinleitungInline, GeltungsbereichInline, VorgabeInline]
|
||||
filter_horizontal=['autoren','pruefende']
|
||||
|
||||
39
dokumente/management/commands/export_xml.py
Normal file
39
dokumente/management/commands/export_xml.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
import xml.etree.ElementTree as ET
|
||||
from dokumente.models import Dokument
|
||||
from dokumente.utils import build_dokument_xml_element, prettify_xml
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Export all dokumente as XML'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--output',
|
||||
type=str,
|
||||
help='Output file path (default: stdout)',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dokumente = Dokument.objects.filter(aktiv=True).prefetch_related(
|
||||
'autoren', 'pruefende', 'vorgaben__thema',
|
||||
'vorgaben__referenzen', 'vorgaben__stichworte',
|
||||
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
|
||||
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
|
||||
'einleitung_set', 'changelog__autoren'
|
||||
).order_by('nummer')
|
||||
|
||||
root = ET.Element('Vorgabendokumente')
|
||||
|
||||
for dokument in dokumente:
|
||||
build_dokument_xml_element(dokument, root)
|
||||
|
||||
xml_str = ET.tostring(root, encoding='unicode', method='xml')
|
||||
xml_output = prettify_xml(xml_str)
|
||||
|
||||
if options['output']:
|
||||
with open(options['output'], 'w', encoding='utf-8') as f:
|
||||
f.write(xml_output)
|
||||
self.stdout.write(self.style.SUCCESS(f'XML exported to {options["output"]}'))
|
||||
else:
|
||||
self.stdout.write(xml_output)
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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'},
|
||||
),
|
||||
]
|
||||
@@ -12,7 +12,7 @@ class Dokumententyp(models.Model):
|
||||
verantwortliche_ve = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
return str(self.name)
|
||||
|
||||
class Meta:
|
||||
verbose_name="Dokumententyp"
|
||||
@@ -28,6 +28,7 @@ class Person(models.Model):
|
||||
class Meta:
|
||||
verbose_name_plural="Personen"
|
||||
ordering = ['name']
|
||||
verbose_name="Person"
|
||||
|
||||
class Thema(models.Model):
|
||||
name = models.CharField(max_length=100, primary_key=True)
|
||||
@@ -37,7 +38,7 @@ class Thema(models.Model):
|
||||
return self.name
|
||||
class Meta:
|
||||
verbose_name_plural="Themen"
|
||||
|
||||
verbose_name="Thema"
|
||||
|
||||
class Dokument(models.Model):
|
||||
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)
|
||||
signatur_cso = models.CharField(max_length=255, blank=True)
|
||||
anhaenge = models.TextField(blank=True)
|
||||
aktiv = models.BooleanField(blank=True)
|
||||
aktiv = models.BooleanField(blank=True,default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.nummer} – {self.name}"
|
||||
@@ -260,12 +261,14 @@ class Geltungsbereich(Textabschnitt):
|
||||
class Meta:
|
||||
verbose_name_plural="Geltungsbereich"
|
||||
verbose_name="Geltungsbereichs-Abschnitt"
|
||||
ordering = ['order']
|
||||
|
||||
class Einleitung(Textabschnitt):
|
||||
einleitung=models.ForeignKey(Dokument,on_delete=models.CASCADE)
|
||||
class Meta:
|
||||
verbose_name_plural="Einleitung"
|
||||
verbose_name="Einleitungs-Abschnitt"
|
||||
ordering = ['order']
|
||||
|
||||
class Checklistenfrage(models.Model):
|
||||
vorgabe=models.ForeignKey(Vorgabe, on_delete=models.CASCADE, related_name="checklistenfragen")
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
|
||||
<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">
|
||||
{% for vorgabe in vorgaben %}
|
||||
{% if vorgabe.checklistenfragen.all %}
|
||||
|
||||
@@ -209,6 +209,11 @@
|
||||
download="{{ standard.nummer }}.json">
|
||||
JSON herunterladen
|
||||
</a>
|
||||
<a href="{% url 'standard_xml' standard.nummer %}"
|
||||
class="btn btn-secondary icon icon--before icon--download"
|
||||
download="{{ standard.nummer }}.xml">
|
||||
XML herunterladen
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1483,6 +1483,248 @@ class JSONExportManagementCommandTest(TestCase):
|
||||
self.assertIn('"Test Standard"', output)
|
||||
|
||||
|
||||
class ExportXMLCommandTest(TestCase):
|
||||
"""Test cases for export_xml management command"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data for XML export"""
|
||||
# Create test data
|
||||
self.dokumententyp = Dokumententyp.objects.create(
|
||||
name="Standard IT-Sicherheit",
|
||||
verantwortliche_ve="SR-SUR-SEC"
|
||||
)
|
||||
|
||||
self.autor1 = Person.objects.create(
|
||||
name="Max Mustermann",
|
||||
funktion="Security Analyst"
|
||||
)
|
||||
self.autor2 = Person.objects.create(
|
||||
name="Erika Mustermann",
|
||||
funktion="Security Manager"
|
||||
)
|
||||
|
||||
self.thema = Thema.objects.create(
|
||||
name="Access Control",
|
||||
erklaerung="Zugangskontrolle"
|
||||
)
|
||||
|
||||
self.dokument = Dokument.objects.create(
|
||||
nummer="TEST-001",
|
||||
dokumententyp=self.dokumententyp,
|
||||
name="Test Standard",
|
||||
gueltigkeit_von=date(2023, 1, 1),
|
||||
gueltigkeit_bis=date(2025, 12, 31),
|
||||
signatur_cso="CSO-123",
|
||||
anhaenge="Anhang1.pdf, Anhang2.pdf",
|
||||
aktiv=True
|
||||
)
|
||||
self.dokument.autoren.add(self.autor1, self.autor2)
|
||||
|
||||
self.vorgabe = Vorgabe.objects.create(
|
||||
order=1,
|
||||
nummer=1,
|
||||
dokument=self.dokument,
|
||||
thema=self.thema,
|
||||
titel="Test Vorgabe",
|
||||
gueltigkeit_von=date(2023, 1, 1),
|
||||
gueltigkeit_bis=date(2025, 12, 31)
|
||||
)
|
||||
|
||||
# Create text sections
|
||||
self.abschnitttyp_text = AbschnittTyp.objects.create(abschnitttyp="text")
|
||||
self.abschnitttyp_table = AbschnittTyp.objects.create(abschnitttyp="table")
|
||||
|
||||
self.geltungsbereich = Geltungsbereich.objects.create(
|
||||
geltungsbereich=self.dokument,
|
||||
abschnitttyp=self.abschnitttyp_text,
|
||||
inhalt="Dies ist der Geltungsbereich",
|
||||
order=1
|
||||
)
|
||||
|
||||
self.einleitung = Einleitung.objects.create(
|
||||
einleitung=self.dokument,
|
||||
abschnitttyp=self.abschnitttyp_text,
|
||||
inhalt="Dies ist die Einleitung",
|
||||
order=1
|
||||
)
|
||||
|
||||
self.kurztext = VorgabeKurztext.objects.create(
|
||||
abschnitt=self.vorgabe,
|
||||
abschnitttyp=self.abschnitttyp_text,
|
||||
inhalt="Dies ist der Kurztext",
|
||||
order=1
|
||||
)
|
||||
|
||||
self.langtext = VorgabeLangtext.objects.create(
|
||||
abschnitt=self.vorgabe,
|
||||
abschnitttyp=self.abschnitttyp_table,
|
||||
inhalt="Spalte1|Spalte2\nWert1|Wert2",
|
||||
order=1
|
||||
)
|
||||
|
||||
self.checklistenfrage = Checklistenfrage.objects.create(
|
||||
vorgabe=self.vorgabe,
|
||||
frage="Ist die Zugriffskontrolle implementiert?"
|
||||
)
|
||||
|
||||
self.changelog = Changelog.objects.create(
|
||||
dokument=self.dokument,
|
||||
datum=date(2023, 6, 1),
|
||||
aenderung="Erste Version erstellt"
|
||||
)
|
||||
self.changelog.autoren.add(self.autor1)
|
||||
|
||||
def test_export_xml_command_stdout(self):
|
||||
"""Test export_xml command output to stdout"""
|
||||
out = StringIO()
|
||||
call_command('export_xml', stdout=out)
|
||||
|
||||
output = out.getvalue()
|
||||
|
||||
# Check that output contains expected XML structure
|
||||
self.assertIn('<Vorgabendokumente>', output)
|
||||
self.assertIn('<Vorgabendokument>', output)
|
||||
self.assertIn('<Typ>Standard IT-Sicherheit</Typ>', output)
|
||||
self.assertIn('<Nummer>TEST-001</Nummer>', output)
|
||||
self.assertIn('<Name>Test Standard</Name>', output)
|
||||
self.assertIn('<Autor>Max Mustermann</Autor>', output)
|
||||
self.assertIn('<Autor>Erika Mustermann</Autor>', output)
|
||||
self.assertIn('<Von>2023-01-01</Von>', output)
|
||||
self.assertIn('<Bis>2025-12-31</Bis>', output)
|
||||
self.assertIn('<SignaturCSO>CSO-123</SignaturCSO>', output)
|
||||
self.assertIn('Dies ist der Geltungsbereich', output)
|
||||
self.assertIn('Dies ist die Einleitung', output)
|
||||
self.assertIn('Dies ist der Kurztext', output)
|
||||
self.assertIn('<Frage>Ist die Zugriffskontrolle implementiert?</Frage>', output)
|
||||
self.assertIn('Erste Version erstellt', output)
|
||||
|
||||
def test_export_xml_command_to_file(self):
|
||||
"""Test export_xml command output to file"""
|
||||
import tempfile
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.xml') as tmp_file:
|
||||
tmp_filename = tmp_file.name
|
||||
|
||||
try:
|
||||
call_command('export_xml', output=tmp_filename)
|
||||
|
||||
# Read file content
|
||||
with open(tmp_filename, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse XML to ensure it's valid
|
||||
root = ET.fromstring(content)
|
||||
|
||||
# Verify structure
|
||||
self.assertEqual(root.tag, 'Vorgabendokumente')
|
||||
|
||||
docs = root.findall('Vorgabendokument')
|
||||
self.assertEqual(len(docs), 1)
|
||||
|
||||
doc = docs[0]
|
||||
self.assertEqual(doc.find('Nummer').text, 'TEST-001')
|
||||
self.assertEqual(doc.find('Name').text, 'Test Standard')
|
||||
self.assertEqual(doc.find('Typ').text, 'Standard IT-Sicherheit')
|
||||
|
||||
autoren = doc.find('Autoren').findall('Autor')
|
||||
self.assertEqual(len(autoren), 2)
|
||||
autor_names = [autor.text for autor in autoren]
|
||||
self.assertIn('Max Mustermann', autor_names)
|
||||
self.assertIn('Erika Mustermann', autor_names)
|
||||
|
||||
finally:
|
||||
# Clean up temporary file
|
||||
if os.path.exists(tmp_filename):
|
||||
os.unlink(tmp_filename)
|
||||
|
||||
def test_export_xml_command_empty_database(self):
|
||||
"""Test export_xml command with no documents"""
|
||||
# Delete all documents
|
||||
Dokument.objects.all().delete()
|
||||
|
||||
out = StringIO()
|
||||
call_command('export_xml', stdout=out)
|
||||
|
||||
output = out.getvalue()
|
||||
|
||||
# Should output empty XML structure (accepts both formats)
|
||||
self.assertIn('Vorgabendokumente', output)
|
||||
self.assertNotIn('<Vorgabendokument>', output)
|
||||
|
||||
def test_export_xml_command_inactive_documents(self):
|
||||
"""Test export_xml command filters inactive documents"""
|
||||
# Create inactive document
|
||||
inactive_doc = Dokument.objects.create(
|
||||
nummer="INACTIVE-001",
|
||||
dokumententyp=self.dokumententyp,
|
||||
name="Inactive Document",
|
||||
aktiv=False
|
||||
)
|
||||
|
||||
out = StringIO()
|
||||
call_command('export_xml', stdout=out)
|
||||
|
||||
output = out.getvalue()
|
||||
|
||||
# Should not contain inactive document
|
||||
self.assertNotIn('INACTIVE-001', output)
|
||||
self.assertNotIn('Inactive Document', output)
|
||||
|
||||
# Should still contain active document
|
||||
self.assertIn('TEST-001', output)
|
||||
self.assertIn('Test Standard', output)
|
||||
|
||||
def test_export_xml_command_table_structure(self):
|
||||
"""Test export_xml command converts markdown tables to proper XML structure"""
|
||||
# Create document with table
|
||||
table_doc = Dokument.objects.create(
|
||||
nummer="TABLE-001",
|
||||
dokumententyp=self.dokumententyp,
|
||||
name="Table Test Document",
|
||||
aktiv=True
|
||||
)
|
||||
table_doc.autoren.add(self.autor1)
|
||||
|
||||
table_vorgabe = Vorgabe.objects.create(
|
||||
order=1,
|
||||
nummer=1,
|
||||
dokument=table_doc,
|
||||
thema=self.thema,
|
||||
titel="Table Test Vorgabe",
|
||||
gueltigkeit_von=date(2023, 1, 1),
|
||||
gueltigkeit_bis=date(2025, 12, 31)
|
||||
)
|
||||
|
||||
table_content = "| Spalte1 | Spalte2 |\n|---------|---------|\n| Wert1 | Wert2 |\n| Wert3 | Wert4 |"
|
||||
|
||||
self.langtext_table = VorgabeLangtext.objects.create(
|
||||
abschnitt=table_vorgabe,
|
||||
abschnitttyp=self.abschnitttyp_table,
|
||||
inhalt=table_content,
|
||||
order=1
|
||||
)
|
||||
|
||||
out = StringIO()
|
||||
call_command('export_xml', stdout=out)
|
||||
|
||||
output = out.getvalue()
|
||||
|
||||
# Check that table structure is properly exported
|
||||
self.assertIn('<table>', output)
|
||||
self.assertIn('<header>', output)
|
||||
self.assertIn('<column>Spalte1</column>', output)
|
||||
self.assertIn('<column>Spalte2</column>', output)
|
||||
self.assertIn('<row>', output)
|
||||
self.assertIn('<column>Wert1</column>', output)
|
||||
self.assertIn('<column>Wert2</column>', output)
|
||||
self.assertIn('<column>Wert3</column>', output)
|
||||
self.assertIn('<column>Wert4</column>', output)
|
||||
# Should not contain the markdown table content as plain text
|
||||
self.assertNotIn('| Spalte1 | Spalte2 |', output)
|
||||
|
||||
|
||||
class StandardJSONViewTest(TestCase):
|
||||
"""Test cases for standard_json view"""
|
||||
|
||||
@@ -1668,6 +1910,261 @@ class StandardJSONViewTest(TestCase):
|
||||
self.assertIn(' ', response.content.decode()) # Check for indentation
|
||||
|
||||
|
||||
class StandardXMLViewTest(TestCase):
|
||||
"""Test cases for standard_xml view"""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test data for XML view"""
|
||||
self.client = Client()
|
||||
|
||||
# Create test data
|
||||
self.dokumententyp = Dokumententyp.objects.create(
|
||||
name="Standard IT-Sicherheit",
|
||||
verantwortliche_ve="SR-SUR-SEC"
|
||||
)
|
||||
|
||||
self.autor = Person.objects.create(
|
||||
name="Test Autor",
|
||||
funktion="Security Analyst"
|
||||
)
|
||||
|
||||
self.pruefender = Person.objects.create(
|
||||
name="Test Pruefender",
|
||||
funktion="Security Manager"
|
||||
)
|
||||
|
||||
self.thema = Thema.objects.create(
|
||||
name="Access Control",
|
||||
erklaerung="Zugangskontrolle"
|
||||
)
|
||||
|
||||
self.dokument = Dokument.objects.create(
|
||||
nummer="XML-001",
|
||||
dokumententyp=self.dokumententyp,
|
||||
name="XML Test Standard",
|
||||
gueltigkeit_von=date(2023, 1, 1),
|
||||
gueltigkeit_bis=date(2025, 12, 31),
|
||||
signatur_cso="CSO-456",
|
||||
anhaenge="test.pdf",
|
||||
aktiv=True
|
||||
)
|
||||
self.dokument.autoren.add(self.autor)
|
||||
self.dokument.pruefende.add(self.pruefender)
|
||||
|
||||
self.vorgabe = Vorgabe.objects.create(
|
||||
order=1,
|
||||
nummer=1,
|
||||
dokument=self.dokument,
|
||||
thema=self.thema,
|
||||
titel="XML Test Vorgabe",
|
||||
gueltigkeit_von=date(2023, 1, 1),
|
||||
gueltigkeit_bis=date(2025, 12, 31)
|
||||
)
|
||||
|
||||
# Create text sections
|
||||
self.abschnitttyp_text = AbschnittTyp.objects.create(abschnitttyp="text")
|
||||
self.abschnitttyp_table = AbschnittTyp.objects.create(abschnitttyp="table")
|
||||
|
||||
self.geltungsbereich = Geltungsbereich.objects.create(
|
||||
geltungsbereich=self.dokument,
|
||||
abschnitttyp=self.abschnitttyp_text,
|
||||
inhalt="XML Geltungsbereich",
|
||||
order=1
|
||||
)
|
||||
|
||||
self.kurztext = VorgabeKurztext.objects.create(
|
||||
abschnitt=self.vorgabe,
|
||||
abschnitttyp=self.abschnitttyp_text,
|
||||
inhalt="XML Kurztext",
|
||||
order=1
|
||||
)
|
||||
|
||||
self.langtext = VorgabeLangtext.objects.create(
|
||||
abschnitt=self.vorgabe,
|
||||
abschnitttyp=self.abschnitttyp_text,
|
||||
inhalt="XML Langtext",
|
||||
order=1
|
||||
)
|
||||
|
||||
self.checklistenfrage = Checklistenfrage.objects.create(
|
||||
vorgabe=self.vorgabe,
|
||||
frage="XML Checklistenfrage?"
|
||||
)
|
||||
|
||||
self.changelog = Changelog.objects.create(
|
||||
dokument=self.dokument,
|
||||
datum=date(2023, 6, 1),
|
||||
aenderung="XML Changelog Eintrag"
|
||||
)
|
||||
self.changelog.autoren.add(self.autor)
|
||||
|
||||
def test_standard_xml_view_success(self):
|
||||
"""Test standard_xml view returns correct XML"""
|
||||
url = reverse('standard_xml', kwargs={'nummer': 'XML-001'})
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'application/xml; charset=utf-8')
|
||||
self.assertIn('attachment', response['Content-Disposition'])
|
||||
self.assertIn('XML-001.xml', response['Content-Disposition'])
|
||||
|
||||
# Parse XML response
|
||||
import xml.etree.ElementTree as ET
|
||||
root = ET.fromstring(response.content)
|
||||
|
||||
# Verify document structure
|
||||
self.assertEqual(root.tag, 'Vorgabendokument')
|
||||
self.assertEqual(root.find('Nummer').text, 'XML-001')
|
||||
self.assertEqual(root.find('Name').text, 'XML Test Standard')
|
||||
self.assertEqual(root.find('Typ').text, 'Standard IT-Sicherheit')
|
||||
self.assertEqual(root.find('Autoren').find('Autor').text, 'Test Autor')
|
||||
self.assertEqual(root.find('Pruefende').find('Pruefender').text, 'Test Pruefender')
|
||||
self.assertEqual(root.find('Gueltigkeit').find('Von').text, '2023-01-01')
|
||||
self.assertEqual(root.find('Gueltigkeit').find('Bis').text, '2025-12-31')
|
||||
self.assertEqual(root.find('SignaturCSO').text, 'CSO-456')
|
||||
self.assertEqual(root.find('Anhaenge').find('Anhang').text, 'test.pdf')
|
||||
self.assertEqual(root.find('Verantwortlich').text, 'Information Security Management BIT')
|
||||
self.assertIn(root.find('Klassifizierung').text, ['', None]) # Empty string or None
|
||||
|
||||
def test_standard_xml_view_not_found(self):
|
||||
"""Test standard_xml view returns 404 for non-existent document"""
|
||||
url = reverse('standard_xml', kwargs={'nummer': 'NONEXISTENT'})
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_standard_xml_view_empty_sections(self):
|
||||
"""Test standard_xml view handles empty sections correctly"""
|
||||
# Create document without sections
|
||||
empty_doc = Dokument.objects.create(
|
||||
nummer="EMPTY-XML-001",
|
||||
dokumententyp=self.dokumententyp,
|
||||
name="Empty Document",
|
||||
aktiv=True
|
||||
)
|
||||
|
||||
url = reverse('standard_xml', kwargs={'nummer': 'EMPTY-XML-001'})
|
||||
response = self.client.get(url)
|
||||
|
||||
# Parse XML response
|
||||
import xml.etree.ElementTree as ET
|
||||
root = ET.fromstring(response.content)
|
||||
|
||||
# Verify empty sections are handled correctly
|
||||
self.assertIsNone(root.find('Geltungsbereich'))
|
||||
self.assertIsNone(root.find('Einleitung'))
|
||||
self.assertEqual(len(root.find('Vorgaben')), 0)
|
||||
self.assertEqual(len(root.find('Changelog')), 0)
|
||||
|
||||
def test_standard_xml_view_null_dates(self):
|
||||
"""Test standard_xml view handles null dates correctly"""
|
||||
# Create document with null dates
|
||||
null_doc = Dokument.objects.create(
|
||||
nummer="NULL-XML-001",
|
||||
dokumententyp=self.dokumententyp,
|
||||
name="Null Dates Document",
|
||||
gueltigkeit_von=None,
|
||||
gueltigkeit_bis=None,
|
||||
aktiv=True
|
||||
)
|
||||
|
||||
url = reverse('standard_xml', kwargs={'nummer': 'NULL-XML-001'})
|
||||
response = self.client.get(url)
|
||||
|
||||
# Parse XML response
|
||||
import xml.etree.ElementTree as ET
|
||||
root = ET.fromstring(response.content)
|
||||
|
||||
# Verify null dates are handled correctly
|
||||
self.assertIn(root.find('Gueltigkeit').find('Von').text, ['', None])
|
||||
self.assertIsNone(root.find('Gueltigkeit').find('Bis').text)
|
||||
|
||||
def test_standard_xml_view_xml_formatting(self):
|
||||
"""Test standard_xml view returns properly formatted XML"""
|
||||
url = reverse('standard_xml', kwargs={'nummer': 'XML-001'})
|
||||
response = self.client.get(url)
|
||||
|
||||
# Check that response is valid XML
|
||||
import xml.etree.ElementTree as ET
|
||||
try:
|
||||
ET.fromstring(response.content)
|
||||
xml_valid = True
|
||||
except ET.ParseError:
|
||||
xml_valid = False
|
||||
|
||||
self.assertTrue(xml_valid)
|
||||
|
||||
# Check that XML is properly indented (should be formatted)
|
||||
self.assertIn('<?xml version', response.content.decode())
|
||||
self.assertIn('\n', response.content.decode())
|
||||
self.assertIn(' ', response.content.decode()) # Check for indentation
|
||||
|
||||
def test_standard_xml_view_table_structure(self):
|
||||
"""Test standard_xml view converts markdown tables to proper XML structure"""
|
||||
# Create document with table
|
||||
table_doc = Dokument.objects.create(
|
||||
nummer="TABLE-XML-001",
|
||||
dokumententyp=self.dokumententyp,
|
||||
name="Table XML Test Document",
|
||||
aktiv=True
|
||||
)
|
||||
table_doc.autoren.add(self.autor)
|
||||
|
||||
table_vorgabe = Vorgabe.objects.create(
|
||||
order=1,
|
||||
nummer=1,
|
||||
dokument=table_doc,
|
||||
thema=self.thema,
|
||||
titel="Table XML Test Vorgabe",
|
||||
gueltigkeit_von=date(2023, 1, 1),
|
||||
gueltigkeit_bis=date(2025, 12, 31)
|
||||
)
|
||||
|
||||
table_content = "| Col1 | Col2 |\n|------|------|\n| A | B |\n| C | D |"
|
||||
|
||||
langtext_table = VorgabeLangtext.objects.create(
|
||||
abschnitt=table_vorgabe,
|
||||
abschnitttyp=self.abschnitttyp_table,
|
||||
inhalt=table_content,
|
||||
order=1
|
||||
)
|
||||
|
||||
url = reverse('standard_xml', kwargs={'nummer': 'TABLE-XML-001'})
|
||||
response = self.client.get(url)
|
||||
|
||||
# Parse XML response
|
||||
import xml.etree.ElementTree as ET
|
||||
root = ET.fromstring(response.content)
|
||||
|
||||
# Find table element
|
||||
table = root.find('.//table')
|
||||
self.assertIsNotNone(table, 'Table element should exist')
|
||||
|
||||
# Check header structure
|
||||
header = table.find('header')
|
||||
self.assertIsNotNone(header, 'Header should exist')
|
||||
header_cols = header.findall('column')
|
||||
self.assertEqual(len(header_cols), 2, 'Header should have 2 columns')
|
||||
self.assertEqual(header_cols[0].text, 'Col1')
|
||||
self.assertEqual(header_cols[1].text, 'Col2')
|
||||
|
||||
# Check row structure
|
||||
rows = table.findall('row')
|
||||
self.assertEqual(len(rows), 2, 'Should have 2 data rows')
|
||||
|
||||
# Check first row
|
||||
row1_cols = rows[0].findall('column')
|
||||
self.assertEqual(len(row1_cols), 2)
|
||||
self.assertEqual(row1_cols[0].text, 'A')
|
||||
self.assertEqual(row1_cols[1].text, 'B')
|
||||
|
||||
# Check second row
|
||||
row2_cols = rows[1].findall('column')
|
||||
self.assertEqual(len(row2_cols), 2)
|
||||
self.assertEqual(row2_cols[0].text, 'C')
|
||||
self.assertEqual(row2_cols[1].text, 'D')
|
||||
|
||||
|
||||
class VorgabeCommentModelTest(TestCase):
|
||||
"""Test cases for VorgabeComment model"""
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ urlpatterns = [
|
||||
path('<str:nummer>/history/', views.standard_detail, {"check_date":"today"}, name='standard_history'),
|
||||
path('<str:nummer>/checkliste/', views.standard_checkliste, name='standard_checkliste'),
|
||||
path('<str:nummer>/json/', views.standard_json, name='standard_json'),
|
||||
path('<str:nummer>/xml/', views.standard_xml, name='standard_xml'),
|
||||
path('comments/<int:vorgabe_id>/', views.get_vorgabe_comments, name='get_vorgabe_comments'),
|
||||
path('comments/<int:vorgabe_id>/add/', views.add_vorgabe_comment, name='add_vorgabe_comment'),
|
||||
path('comments/delete/<int:comment_id>/', views.delete_vorgabe_comment, name='delete_vorgabe_comment'),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""
|
||||
Utility functions for Vorgaben sanity checking
|
||||
Utility functions for Vorgaben sanity checking and XML export
|
||||
"""
|
||||
import datetime
|
||||
import xml.etree.ElementTree as ET
|
||||
import xml.dom.minidom
|
||||
from django.db.models import Count
|
||||
from itertools import combinations
|
||||
from dokumente.models import Vorgabe
|
||||
@@ -121,3 +123,190 @@ def format_conflict_report(conflicts, verbose=False):
|
||||
lines.append(f" Overlap starts: {overlap_start} (no end)")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# XML Export utilities
|
||||
|
||||
def parse_markdown_table(markdown_content):
|
||||
"""
|
||||
Parse markdown table content and return XML element with <table><header><row><column> structure
|
||||
"""
|
||||
lines = [line.strip() for line in markdown_content.strip().split('\n') if line.strip()]
|
||||
if not lines:
|
||||
return None
|
||||
|
||||
# Create table element
|
||||
table = ET.Element('table')
|
||||
|
||||
# Parse first row as header
|
||||
header_row = [cell.strip() for cell in lines[0].split('|') if cell.strip()]
|
||||
header = ET.SubElement(table, 'header')
|
||||
for cell in header_row:
|
||||
column = ET.SubElement(header, 'column')
|
||||
column.text = cell
|
||||
|
||||
# Parse remaining rows (skip separator row if it exists)
|
||||
for line in lines[2:] if len(lines) > 1 and all(c in '-| ' for c in lines[1]) else lines[1:]:
|
||||
# Check if this is a separator row
|
||||
if all(c in '-| ' for c in line):
|
||||
continue
|
||||
|
||||
row = ET.SubElement(table, 'row')
|
||||
row_cells = [cell.strip() for cell in line.split('|') if cell.strip()]
|
||||
for cell in row_cells:
|
||||
column = ET.SubElement(row, 'column')
|
||||
column.text = cell
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def prettify_xml(xml_string):
|
||||
"""
|
||||
Prettify XML string with proper indentation
|
||||
"""
|
||||
dom = xml.dom.minidom.parseString(xml_string)
|
||||
return dom.toprettyxml(indent=" ", encoding="UTF-8").decode('utf-8')
|
||||
|
||||
|
||||
def build_dokument_xml_element(dokument, parent_element):
|
||||
"""
|
||||
Build XML element for a single Dokument and append it to parent_element.
|
||||
|
||||
Args:
|
||||
dokument: Dokument instance (should be prefetched with related data)
|
||||
parent_element: Parent XML element to append to
|
||||
|
||||
Returns:
|
||||
The created document element
|
||||
"""
|
||||
doc_element = ET.SubElement(parent_element, 'Vorgabendokument')
|
||||
|
||||
ET.SubElement(doc_element, 'Typ').text = dokument.dokumententyp.name if dokument.dokumententyp else ""
|
||||
ET.SubElement(doc_element, 'Nummer').text = dokument.nummer
|
||||
ET.SubElement(doc_element, 'Name').text = dokument.name
|
||||
|
||||
autoren_element = ET.SubElement(doc_element, 'Autoren')
|
||||
for autor in dokument.autoren.all():
|
||||
ET.SubElement(autoren_element, 'Autor').text = autor.name
|
||||
|
||||
pruefende_element = ET.SubElement(doc_element, 'Pruefende')
|
||||
for pruefender in dokument.pruefende.all():
|
||||
ET.SubElement(pruefende_element, 'Pruefender').text = pruefender.name
|
||||
|
||||
gueltigkeit_element = ET.SubElement(doc_element, 'Gueltigkeit')
|
||||
ET.SubElement(gueltigkeit_element, 'Von').text = dokument.gueltigkeit_von.strftime("%Y-%m-%d") if dokument.gueltigkeit_von else ""
|
||||
ET.SubElement(gueltigkeit_element, 'Bis').text = dokument.gueltigkeit_bis.strftime("%Y-%m-%d") if dokument.gueltigkeit_bis else None
|
||||
|
||||
ET.SubElement(doc_element, 'SignaturCSO').text = dokument.signatur_cso
|
||||
|
||||
geltungsbereich_sections = dokument.geltungsbereich_set.all().order_by('order')
|
||||
if geltungsbereich_sections:
|
||||
geltungsbereich_element = ET.SubElement(doc_element, 'Geltungsbereich')
|
||||
for gb in geltungsbereich_sections:
|
||||
section_type = gb.abschnitttyp.abschnitttyp if gb.abschnitttyp else "text"
|
||||
if section_type in ('tabelle', 'table'):
|
||||
table = parse_markdown_table(gb.inhalt)
|
||||
if table is not None:
|
||||
abschnitt_element = ET.SubElement(geltungsbereich_element, 'Abschnitt')
|
||||
abschnitt_element.set('typ', section_type)
|
||||
abschnitt_element.append(table)
|
||||
else:
|
||||
abschnitt_element = ET.SubElement(geltungsbereich_element, 'Abschnitt')
|
||||
abschnitt_element.set('typ', section_type)
|
||||
abschnitt_element.text = gb.inhalt
|
||||
|
||||
einleitung_sections = dokument.einleitung_set.all().order_by('order')
|
||||
if einleitung_sections:
|
||||
einleitung_element = ET.SubElement(doc_element, 'Einleitung')
|
||||
for ei in einleitung_sections:
|
||||
section_type = ei.abschnitttyp.abschnitttyp if ei.abschnitttyp else "text"
|
||||
if section_type in ('tabelle', 'table'):
|
||||
table = parse_markdown_table(ei.inhalt)
|
||||
if table is not None:
|
||||
abschnitt_element = ET.SubElement(einleitung_element, 'Abschnitt')
|
||||
abschnitt_element.set('typ', section_type)
|
||||
abschnitt_element.append(table)
|
||||
else:
|
||||
abschnitt_element = ET.SubElement(einleitung_element, 'Abschnitt')
|
||||
abschnitt_element.set('typ', section_type)
|
||||
abschnitt_element.text = ei.inhalt
|
||||
|
||||
ET.SubElement(doc_element, 'Ziel').text = ""
|
||||
ET.SubElement(doc_element, 'Grundlagen').text = ""
|
||||
|
||||
changelog_element = ET.SubElement(doc_element, 'Changelog')
|
||||
for cl in dokument.changelog.all().order_by('-datum'):
|
||||
entry = ET.SubElement(changelog_element, 'Eintrag')
|
||||
ET.SubElement(entry, 'Datum').text = cl.datum.strftime("%Y-%m-%d")
|
||||
autoren = ET.SubElement(entry, 'Autoren')
|
||||
for autor in cl.autoren.all():
|
||||
ET.SubElement(autoren, 'Autor').text = autor.name
|
||||
ET.SubElement(entry, 'Aenderung').text = cl.aenderung
|
||||
|
||||
anhaenge_element = ET.SubElement(doc_element, 'Anhaenge')
|
||||
ET.SubElement(anhaenge_element, 'Anhang').text = dokument.anhaenge
|
||||
|
||||
ET.SubElement(doc_element, 'Verantwortlich').text = "Information Security Management BIT"
|
||||
ET.SubElement(doc_element, 'Klassifizierung').text = ""
|
||||
|
||||
glossar_element = ET.SubElement(doc_element, 'Glossar')
|
||||
|
||||
vorgaben_element = ET.SubElement(doc_element, 'Vorgaben')
|
||||
|
||||
for vorgabe in dokument.vorgaben.all().order_by('order'):
|
||||
vorgabe_el = ET.SubElement(vorgaben_element, 'Vorgabe')
|
||||
|
||||
ET.SubElement(vorgabe_el, 'Nummer').text = str(vorgabe.nummer)
|
||||
ET.SubElement(vorgabe_el, 'Titel').text = vorgabe.titel
|
||||
ET.SubElement(vorgabe_el, 'Thema').text = vorgabe.thema.name if vorgabe.thema else ""
|
||||
|
||||
kurztext_sections = vorgabe.vorgabekurztext_set.all().order_by('order')
|
||||
if kurztext_sections:
|
||||
kurztext_element = ET.SubElement(vorgabe_el, 'Kurztext')
|
||||
for kt in kurztext_sections:
|
||||
section_type = kt.abschnitttyp.abschnitttyp if kt.abschnitttyp else "text"
|
||||
if section_type in ('tabelle', 'table'):
|
||||
table = parse_markdown_table(kt.inhalt)
|
||||
if table is not None:
|
||||
abschnitt = ET.SubElement(kurztext_element, 'Abschnitt')
|
||||
abschnitt.set('typ', section_type)
|
||||
abschnitt.append(table)
|
||||
else:
|
||||
abschnitt = ET.SubElement(kurztext_element, 'Abschnitt')
|
||||
abschnitt.set('typ', section_type)
|
||||
abschnitt.text = kt.inhalt
|
||||
|
||||
langtext_sections = vorgabe.vorgabelangtext_set.all().order_by('order')
|
||||
if langtext_sections:
|
||||
langtext_element = ET.SubElement(vorgabe_el, 'Langtext')
|
||||
for lt in langtext_sections:
|
||||
section_type = lt.abschnitttyp.abschnitttyp if lt.abschnitttyp else "text"
|
||||
if section_type in ('tabelle', 'table'):
|
||||
table = parse_markdown_table(lt.inhalt)
|
||||
if table is not None:
|
||||
abschnitt = ET.SubElement(langtext_element, 'Abschnitt')
|
||||
abschnitt.set('typ', section_type)
|
||||
abschnitt.append(table)
|
||||
else:
|
||||
abschnitt = ET.SubElement(langtext_element, 'Abschnitt')
|
||||
abschnitt.set('typ', section_type)
|
||||
abschnitt.text = lt.inhalt
|
||||
|
||||
referenz_element = ET.SubElement(vorgabe_el, 'Referenzen')
|
||||
for ref in vorgabe.referenzen.all():
|
||||
ref_text = f"{ref.name_nummer}: {ref.name_text}" if ref.name_text else ref.name_nummer
|
||||
ET.SubElement(referenz_element, 'Referenz').text = ref_text
|
||||
|
||||
vorgabe_gueltigkeit = ET.SubElement(vorgabe_el, 'Gueltigkeit')
|
||||
ET.SubElement(vorgabe_gueltigkeit, 'Von').text = vorgabe.gueltigkeit_von.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_von else ""
|
||||
ET.SubElement(vorgabe_gueltigkeit, 'Bis').text = vorgabe.gueltigkeit_bis.strftime("%Y-%m-%d") if vorgabe.gueltigkeit_bis else None
|
||||
|
||||
checklistenfragen_element = ET.SubElement(vorgabe_el, 'Checklistenfragen')
|
||||
for cf in vorgabe.checklistenfragen.all():
|
||||
ET.SubElement(checklistenfragen_element, 'Frage').text = cf.frage
|
||||
|
||||
stichworte_element = ET.SubElement(vorgabe_el, 'Stichworte')
|
||||
for stw in vorgabe.stichworte.all():
|
||||
ET.SubElement(stichworte_element, 'Stichwort').text = stw.stichwort
|
||||
|
||||
return doc_element
|
||||
@@ -1,13 +1,15 @@
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.http import JsonResponse
|
||||
from django.http import JsonResponse, HttpResponse
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.html import escape, mark_safe
|
||||
from django.utils.safestring import SafeString
|
||||
import json
|
||||
import xml.etree.ElementTree as ET
|
||||
from .models import Dokument, Vorgabe, VorgabeKurztext, VorgabeLangtext, Checklistenfrage, VorgabeComment
|
||||
from .utils import build_dokument_xml_element, prettify_xml
|
||||
from abschnitte.utils import render_textabschnitte
|
||||
|
||||
from datetime import date
|
||||
@@ -254,6 +256,37 @@ def standard_json(request, nummer):
|
||||
return JsonResponse(doc_data, json_dumps_params={'indent': 2, 'ensure_ascii': False}, encoder=DjangoJSONEncoder)
|
||||
|
||||
|
||||
def standard_xml(request, nummer):
|
||||
"""
|
||||
Export a single Dokument as XML
|
||||
"""
|
||||
# Get the document with all related data
|
||||
dokument = get_object_or_404(
|
||||
Dokument.objects.prefetch_related(
|
||||
'autoren', 'pruefende', 'vorgaben__thema',
|
||||
'vorgaben__referenzen', 'vorgaben__stichworte',
|
||||
'vorgaben__checklistenfragen', 'vorgaben__vorgabekurztext_set',
|
||||
'vorgaben__vorgabelangtext_set', 'geltungsbereich_set',
|
||||
'einleitung_set', 'changelog__autoren'
|
||||
),
|
||||
nummer=nummer
|
||||
)
|
||||
|
||||
# Create a temporary root element to build the document
|
||||
root = ET.Element('root')
|
||||
build_dokument_xml_element(dokument, root)
|
||||
|
||||
# Get the actual document element (first child of root)
|
||||
doc_element = root[0]
|
||||
|
||||
xml_str = ET.tostring(doc_element, encoding='unicode', method='xml')
|
||||
xml_output = prettify_xml(xml_str)
|
||||
|
||||
response = HttpResponse(xml_output, content_type='application/xml; charset=utf-8')
|
||||
response['Content-Disposition'] = f'attachment; filename="{dokument.nummer}.xml"'
|
||||
return response
|
||||
|
||||
|
||||
@login_required
|
||||
def get_vorgabe_comments(request, vorgabe_id):
|
||||
"""Get comments for a specific Vorgabe"""
|
||||
|
||||
@@ -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.
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
13
k8s/pvc.yaml
13
k8s/pvc.yaml
@@ -1,13 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: django-data-pvc
|
||||
namespace: vorgabenui
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
storageClassName: nfs
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
apiVersion: traefik.containo.us/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: vorgabenui-rewrite
|
||||
namespace: vorgabenui
|
||||
spec:
|
||||
stripPrefix:
|
||||
prefixes:
|
||||
- "/"
|
||||
16
pages/templates/400.html
Normal file
16
pages/templates/400.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Ungültige Anfrage{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="alert alert-warning">
|
||||
<h2><i class="icon icon--alert"></i> Ungültige Anfrage (400)</h2>
|
||||
<p>Ihre Anfrage konnte nicht verarbeitet werden.</p>
|
||||
<p>Bitte überprüfen Sie die eingegebenen Daten und versuchen Sie es erneut.</p>
|
||||
<p><a href="/" class="btn btn-primary">Zur Startseite</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
21
pages/templates/403.html
Normal file
21
pages/templates/403.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Zugriff verweigert{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="alert alert-warning">
|
||||
<h2><i class="icon icon--alert"></i> Zugriff verweigert (403)</h2>
|
||||
<p>Sie haben keine Berechtigung, auf diese Seite zuzugreifen.</p>
|
||||
<p>Bitte melden Sie sich an oder wenden Sie sich an den Administrator.</p>
|
||||
<p>
|
||||
<a href="/" class="btn btn-primary">Zur Startseite</a>
|
||||
{% if not user.is_authenticated %}
|
||||
<a href="{% url 'login' %}" class="btn btn-secondary">Anmelden</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
21
pages/templates/404.html
Normal file
21
pages/templates/404.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Seite nicht gefunden{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="alert alert-danger">
|
||||
<h2><i class="icon icon--alert"></i> Seite nicht gefunden (404)</h2>
|
||||
<p>Die gewünschte Seite konnte nicht gefunden werden.</p>
|
||||
<p>Mögliche Gründe:</p>
|
||||
<ul>
|
||||
<li>Sie haben eine falsche URL eingegeben</li>
|
||||
<li>Die Seite wurde verschoben oder gelöscht</li>
|
||||
<li>Sie haben keine Berechtigung für diese Seite</li>
|
||||
</ul>
|
||||
<p><a href="/" class="btn btn-primary">Zur Startseite</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
16
pages/templates/500.html
Normal file
16
pages/templates/500.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Serverfehler{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="alert alert-danger">
|
||||
<h2><i class="icon icon--alert"></i> Serverfehler (500)</h2>
|
||||
<p>Bei der Verarbeitung Ihrer Anfrage ist ein interner Fehler aufgetreten.</p>
|
||||
<p>Der Administrator wurde über dieses Problem informiert.</p>
|
||||
<p><a href="/" class="btn btn-primary">Zur Startseite</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -219,7 +219,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-6 text-right">
|
||||
<p class="text-muted">Version {{ version|default:"0.973" }}</p>
|
||||
<p class="text-muted">Version {{ version|default:"0.986" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,3 +69,15 @@ def search(request):
|
||||
|
||||
return render(request,"results.html",{"suchbegriff":safe_search_term,"resultat":result})
|
||||
|
||||
def custom_400(request, exception):
|
||||
return render(request, '400.html', status=400)
|
||||
|
||||
def custom_403(request, exception):
|
||||
return render(request, '403.html', status=403)
|
||||
|
||||
def custom_404(request, exception):
|
||||
return render(request, '404.html', status=404)
|
||||
|
||||
def custom_500(request):
|
||||
return render(request, '500.html', status=500)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 .models import *
|
||||
|
||||
@@ -14,7 +14,7 @@ class ReferenzerklaerungInline(NestedStackedInline):
|
||||
class ReferenzAdmin(NestedModelAdmin):
|
||||
list_display = ('Path', 'vorgaben_count')
|
||||
inlines=[ReferenzerklaerungInline]
|
||||
search_fields=("name_nummer","Path")
|
||||
search_fields=("name_nummer","name_text")
|
||||
readonly_fields=("vorgaben_list",)
|
||||
fieldsets = (
|
||||
(None, {
|
||||
@@ -34,7 +34,7 @@ class ReferenzAdmin(NestedModelAdmin):
|
||||
vorgaben_list = list(vorgaben) # Evaluate queryset once
|
||||
count = len(vorgaben_list)
|
||||
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 += "<table style='width: 100%; border-collapse: collapse;'>"
|
||||
@@ -55,7 +55,7 @@ class ReferenzAdmin(NestedModelAdmin):
|
||||
html += "</tbody></table>"
|
||||
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"
|
||||
|
||||
|
||||
17
referenzen/migrations/0004_alter_referenz_options.py
Normal file
17
referenzen/migrations/0004_alter_referenz_options.py
Normal 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'},
|
||||
),
|
||||
]
|
||||
@@ -13,18 +13,21 @@ class Referenz(MPTTModel):
|
||||
url = models.URLField(blank=True)
|
||||
|
||||
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 "")
|
||||
return Temp
|
||||
path = " → ".join([x.name_nummer for x in self.get_ancestors(include_self=True)])
|
||||
if self.name_text:
|
||||
path += " (%s)" % self.name_text
|
||||
return path
|
||||
|
||||
class MPTTMeta:
|
||||
parent_attr = 'oberreferenz' # optional, but safe
|
||||
order_insertion_by = ['name_nummer']
|
||||
|
||||
def __str__(self):
|
||||
return self.name_nummer
|
||||
return self.Path()
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural="Referenzen"
|
||||
verbose_name="Referenz"
|
||||
|
||||
class Referenzerklaerung (Textabschnitt):
|
||||
erklaerung = models.ForeignKey(Referenz,on_delete=models.CASCADE)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.shortcuts import render
|
||||
from .models import Referenz
|
||||
from abschnitte.utils import render_textabschnitte
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
# Create your views here.
|
||||
def tree(request):
|
||||
@@ -9,7 +10,7 @@ def tree(request):
|
||||
|
||||
|
||||
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.children = list(referenz_item.get_descendants(include_self=True))
|
||||
for child in referenz_item.children:
|
||||
|
||||
@@ -1,36 +1,44 @@
|
||||
appdirs==1.4.4
|
||||
asgiref==3.8.1
|
||||
blessed==1.21.0
|
||||
certifi==2025.8.3
|
||||
charset-normalizer==3.4.3
|
||||
asgiref==3.11.0
|
||||
bleach==6.3.0
|
||||
blessed==1.27.0
|
||||
certifi==2026.1.4
|
||||
charset-normalizer==3.4.4
|
||||
coverage==7.13.1
|
||||
curtsies==0.4.3
|
||||
cwcwidth==0.1.10
|
||||
Django==5.2.9
|
||||
django-admin-sortable2==2.2.8
|
||||
cwcwidth==0.1.12
|
||||
Django==6.0.1
|
||||
django-admin-sortable2==2.3
|
||||
django-js-asset==3.1.2
|
||||
django-mptt==0.17.0
|
||||
django-mptt-admin==2.8.0
|
||||
django-nested-admin==4.1.1
|
||||
django-mptt==0.18.0
|
||||
django-mptt-admin==2.9.0
|
||||
django-nested-admin==4.1.6
|
||||
django-nested-inline==0.4.6
|
||||
django-revproxy==0.13.0
|
||||
greenlet==3.2.4
|
||||
greenlet==3.3.0
|
||||
gunicorn==23.0.0
|
||||
idna==3.10
|
||||
idna==3.11
|
||||
jedi==0.19.2
|
||||
Markdown==3.8.2
|
||||
jproperties==2.1.2
|
||||
Markdown==3.10
|
||||
packaging==25.0
|
||||
parsedatetime==2.6
|
||||
parso==0.8.4
|
||||
parso==0.8.5
|
||||
pep8==1.7.1
|
||||
prompt_toolkit==3.0.51
|
||||
prompt_toolkit==3.0.52
|
||||
pyfakefs==5.9.3
|
||||
Pygments==2.19.2
|
||||
pysonar==1.2.1.3951
|
||||
python-dateutil==2.9.0.post0
|
||||
python-monkey-business==1.1.0
|
||||
pyxdg==0.28
|
||||
PyYAML==6.0.3
|
||||
requests==2.32.5
|
||||
responses==0.25.8
|
||||
six==1.17.0
|
||||
sqlparse==0.5.3
|
||||
urllib3==2.6.0
|
||||
wcwidth==0.2.13
|
||||
bleach==6.1.0
|
||||
coverage==7.6.1
|
||||
sqlparse==0.5.5
|
||||
tomli==2.2.1
|
||||
urllib3==2.6.3
|
||||
wcwidth==0.2.14
|
||||
webencodings==0.5.1
|
||||
whitenoise==6.11.0
|
||||
|
||||
17
rollen/migrations/0002_alter_rolle_options.py
Normal file
17
rollen/migrations/0002_alter_rolle_options.py
Normal 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)'},
|
||||
),
|
||||
]
|
||||
17
rollen/migrations/0003_alter_rolle_options.py
Normal file
17
rollen/migrations/0003_alter_rolle_options.py
Normal 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'},
|
||||
),
|
||||
]
|
||||
@@ -9,6 +9,7 @@ class Rolle(models.Model):
|
||||
return self.name
|
||||
class Meta:
|
||||
verbose_name_plural="Rollen"
|
||||
verbose_name="Rolle"
|
||||
|
||||
class RollenBeschreibung(Textabschnitt):
|
||||
abschnitt=models.ForeignKey(Rolle,on_delete=models.CASCADE)
|
||||
|
||||
216
scripts/deploy-argocd-configmap.sh
Executable file
216
scripts/deploy-argocd-configmap.sh
Executable file
@@ -0,0 +1,216 @@
|
||||
#!/bin/bash
|
||||
|
||||
# deploy-argocd-configmap.sh
|
||||
# Script to deploy Django ConfigMap to vorgabenui namespace for ArgoCD
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ArgoCD-specific configuration (hardcoded for consistency)
|
||||
NAMESPACE="vorgabenui"
|
||||
CONFIGMAP_NAME="django-config"
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
ARGOCD_DIR="$SCRIPT_DIR/../argocd"
|
||||
CONFIGMAP_FILE="$ARGOCD_DIR/configmap.yaml"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
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"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${BLUE}[STEP]${NC} $1"
|
||||
}
|
||||
|
||||
# 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 configmap file exists
|
||||
check_configmap_file() {
|
||||
if [ ! -f "$CONFIGMAP_FILE" ]; then
|
||||
log_error "ConfigMap file not found: $CONFIGMAP_FILE"
|
||||
log_error "Expected ArgoCD ConfigMap file at: $CONFIGMAP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to deploy the configmap
|
||||
deploy_configmap() {
|
||||
log_step "Deploying ConfigMap '$CONFIGMAP_NAME' to namespace '$NAMESPACE'..."
|
||||
|
||||
kubectl apply -f "$CONFIGMAP_FILE"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "Successfully deployed ConfigMap '$CONFIGMAP_NAME'"
|
||||
return 0
|
||||
else
|
||||
log_error "Failed to deploy ConfigMap '$CONFIGMAP_NAME'"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to verify the configmap
|
||||
verify_configmap() {
|
||||
log_step "Verifying ConfigMap deployment..."
|
||||
|
||||
if kubectl get configmap "$CONFIGMAP_NAME" --namespace="$NAMESPACE" &> /dev/null; then
|
||||
log_info "✅ ConfigMap '$CONFIGMAP_NAME' exists in namespace '$NAMESPACE'"
|
||||
|
||||
echo ""
|
||||
log_info "ConfigMap details:"
|
||||
kubectl describe configmap "$CONFIGMAP_NAME" --namespace="$NAMESPACE"
|
||||
|
||||
echo ""
|
||||
log_info "ConfigMap data:"
|
||||
kubectl get configmap "$CONFIGMAP_NAME" --namespace="$NAMESPACE" -o yaml | grep -A 20 "^data:"
|
||||
|
||||
return 0
|
||||
else
|
||||
log_error "❌ ConfigMap '$CONFIGMAP_NAME' not found in namespace '$NAMESPACE'"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to show usage
|
||||
show_usage() {
|
||||
echo "ArgoCD ConfigMap Deployment Script for VorgabenUI"
|
||||
echo ""
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "This script deploys Django configuration to the vorgabenui namespace for ArgoCD."
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -h, --help Show this help message"
|
||||
echo " --verify-only Only verify existing ConfigMap, don't deploy"
|
||||
echo " --dry-run Show what would be deployed without applying"
|
||||
echo ""
|
||||
echo "Configuration (hardcoded for ArgoCD):"
|
||||
echo " Namespace: $NAMESPACE"
|
||||
echo " ConfigMap Name: $CONFIGMAP_NAME"
|
||||
echo " ConfigMap File: $CONFIGMAP_FILE"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Deploy ConfigMap"
|
||||
echo " $0 --verify-only # Verify existing ConfigMap"
|
||||
echo " $0 --dry-run # Preview deployment"
|
||||
echo ""
|
||||
echo "Note: Run this before deploying the ArgoCD deployment to ensure configuration is available."
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
VERIFY_ONLY=false
|
||||
DRY_RUN=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--verify-only)
|
||||
VERIFY_ONLY=true
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
echo ""
|
||||
log_info "🚀 ArgoCD Django ConfigMap Deployment Script"
|
||||
log_info "============================================"
|
||||
echo ""
|
||||
log_info "Target Configuration:"
|
||||
log_info " Namespace: $NAMESPACE"
|
||||
log_info " ConfigMap Name: $CONFIGMAP_NAME"
|
||||
log_info " ConfigMap File: $CONFIGMAP_FILE"
|
||||
echo ""
|
||||
|
||||
# Perform checks
|
||||
log_step "Performing pre-flight checks..."
|
||||
check_kubectl
|
||||
check_configmap_file
|
||||
log_info "✅ All pre-flight checks passed"
|
||||
echo ""
|
||||
|
||||
# Verify-only mode
|
||||
if [ "$VERIFY_ONLY" = true ]; then
|
||||
log_info "🔍 Verify-only mode - checking existing ConfigMap"
|
||||
verify_configmap
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# Dry-run mode
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log_info "🔍 Dry-run mode - showing what would be deployed:"
|
||||
echo ""
|
||||
log_info "ConfigMap content that would be deployed:"
|
||||
cat "$CONFIGMAP_FILE"
|
||||
echo ""
|
||||
log_info "Would run: kubectl apply -f $CONFIGMAP_FILE"
|
||||
echo ""
|
||||
log_info "Run without --dry-run to execute the deployment"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 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"
|
||||
log_info "✅ Created namespace '$NAMESPACE'"
|
||||
fi
|
||||
|
||||
# Deploy the ConfigMap
|
||||
if deploy_configmap; then
|
||||
echo ""
|
||||
# Verify deployment
|
||||
verify_configmap
|
||||
echo ""
|
||||
|
||||
log_info "🎉 ConfigMap deployment completed successfully!"
|
||||
echo ""
|
||||
log_info "📋 Next steps:"
|
||||
log_info "1. Deploy the secret (if not already done):"
|
||||
echo " ./scripts/deploy-argocd-secret.sh"
|
||||
echo ""
|
||||
log_info "2. Apply the updated deployment:"
|
||||
echo " kubectl apply -f argocd/deployment.yaml"
|
||||
echo ""
|
||||
log_info "3. Verify Django pods start with proper configuration"
|
||||
echo ""
|
||||
else
|
||||
log_error "ConfigMap deployment failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
311
scripts/deploy-argocd-secret.sh
Executable file
311
scripts/deploy-argocd-secret.sh
Executable file
@@ -0,0 +1,311 @@
|
||||
#!/bin/bash
|
||||
|
||||
# deploy-argocd-secret.sh
|
||||
# ArgoCD-specific script to generate and deploy Django SECRET_KEY to vorgabenui namespace
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ArgoCD-specific configuration (hardcoded for consistency)
|
||||
NAMESPACE="vorgabenui"
|
||||
SECRET_NAME="vorgabenui-secrets"
|
||||
SECRET_KEY_NAME="vorgabenui_secret"
|
||||
SCRIPT_DIR="$(dirname "$0")"
|
||||
ARGOCD_DIR="$SCRIPT_DIR/../argocd"
|
||||
TEMPLATES_DIR="$SCRIPT_DIR/../templates"
|
||||
SECRET_TEMPLATE="$TEMPLATES_DIR/secret.yaml"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
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"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${BLUE}[STEP]${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 check if secret template exists
|
||||
check_template() {
|
||||
if [ ! -f "$SECRET_TEMPLATE" ]; then
|
||||
# Fallback to argocd directory if templates directory doesn't exist
|
||||
FALLBACK_TEMPLATE="$ARGOCD_DIR/secret.yaml"
|
||||
if [ -f "$FALLBACK_TEMPLATE" ]; then
|
||||
SECRET_TEMPLATE="$FALLBACK_TEMPLATE"
|
||||
log_warn "Using fallback template: $SECRET_TEMPLATE"
|
||||
else
|
||||
log_error "Secret template not found at either:"
|
||||
log_error " Primary: $TEMPLATES_DIR/secret.yaml"
|
||||
log_error " Fallback: $FALLBACK_TEMPLATE"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create the secret
|
||||
create_secret() {
|
||||
local secret_key="$1"
|
||||
|
||||
log_step "Creating Kubernetes secret '$SECRET_NAME' in namespace '$NAMESPACE'..."
|
||||
|
||||
# Create the secret directly with kubectl (this will create or update)
|
||||
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'"
|
||||
return 0
|
||||
else
|
||||
log_error "Failed to create/update secret '$SECRET_NAME'"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to verify the secret
|
||||
verify_secret() {
|
||||
log_step "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 metadata (without revealing the actual key)
|
||||
echo ""
|
||||
log_info "Secret details:"
|
||||
kubectl describe secret "$SECRET_NAME" --namespace="$NAMESPACE" | grep -E "^(Name|Namespace|Type|Data)"
|
||||
|
||||
# Verify the key exists in the secret
|
||||
if kubectl get secret "$SECRET_NAME" --namespace="$NAMESPACE" -o jsonpath="{.data.$SECRET_KEY_NAME}" &> /dev/null; then
|
||||
log_info "✅ Secret key '$SECRET_KEY_NAME' is present in the secret"
|
||||
return 0
|
||||
else
|
||||
log_error "❌ Secret key '$SECRET_KEY_NAME' not found in secret"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_error "❌ Secret '$SECRET_NAME' not found in namespace '$NAMESPACE'"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to test secret in pod (if deployment exists)
|
||||
test_secret_in_pod() {
|
||||
log_step "Testing secret accessibility in Django deployment..."
|
||||
|
||||
# Check if Django deployment exists
|
||||
if kubectl get deployment django --namespace="$NAMESPACE" &> /dev/null; then
|
||||
log_info "Django deployment found, testing secret access..."
|
||||
|
||||
# Try to get the secret value from a pod (this will fail if env var not configured)
|
||||
local pod_name
|
||||
pod_name=$(kubectl get pods -l app=django --namespace="$NAMESPACE" -o jsonpath="{.items[0].metadata.name}" 2>/dev/null)
|
||||
|
||||
if [ -n "$pod_name" ] && [ "$pod_name" != "" ]; then
|
||||
log_info "Testing secret in pod: $pod_name"
|
||||
if kubectl exec "$pod_name" --namespace="$NAMESPACE" -- printenv VORGABENUI_SECRET &> /dev/null; then
|
||||
log_info "✅ VORGABENUI_SECRET environment variable is accessible in pod"
|
||||
else
|
||||
log_warn "⚠️ VORGABENUI_SECRET environment variable not found in pod"
|
||||
log_warn " This is expected if the deployment hasn't been updated yet"
|
||||
fi
|
||||
else
|
||||
log_warn "⚠️ No running Django pods found"
|
||||
fi
|
||||
else
|
||||
log_info "Django deployment not found - secret will be available when deployment is updated"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to show usage
|
||||
show_usage() {
|
||||
echo "ArgoCD Secret Deployment Script for VorgabenUI"
|
||||
echo ""
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo ""
|
||||
echo "This script deploys Django SECRET_KEY to the vorgabenui namespace for ArgoCD."
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " -h, --help Show this help message"
|
||||
echo " --verify-only Only verify existing secret, don't create new one"
|
||||
echo " --dry-run Show what would be done without making changes"
|
||||
echo ""
|
||||
echo "Configuration (hardcoded for ArgoCD):"
|
||||
echo " Namespace: $NAMESPACE"
|
||||
echo " Secret Name: $SECRET_NAME"
|
||||
echo " Secret Key: $SECRET_KEY_NAME"
|
||||
echo " Template: $SECRET_TEMPLATE"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Generate and deploy new secret"
|
||||
echo " $0 --verify-only # Verify existing secret"
|
||||
echo " $0 --dry-run # Preview changes"
|
||||
echo ""
|
||||
echo "After running this script, update argocd/deployment.yaml to reference the secret."
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
VERIFY_ONLY=false
|
||||
DRY_RUN=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--verify-only)
|
||||
VERIFY_ONLY=true
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
echo ""
|
||||
log_info "🚀 ArgoCD Django SECRET_KEY Deployment Script"
|
||||
log_info "============================================="
|
||||
echo ""
|
||||
log_info "Target Configuration:"
|
||||
log_info " Namespace: $NAMESPACE"
|
||||
log_info " Secret Name: $SECRET_NAME"
|
||||
log_info " Secret Key Name: $SECRET_KEY_NAME"
|
||||
echo ""
|
||||
|
||||
# Perform checks
|
||||
log_step "Performing pre-flight checks..."
|
||||
check_kubectl
|
||||
check_python
|
||||
check_template
|
||||
log_info "✅ All pre-flight checks passed"
|
||||
echo ""
|
||||
|
||||
# Verify-only mode
|
||||
if [ "$VERIFY_ONLY" = true ]; then
|
||||
log_info "🔍 Verify-only mode - checking existing secret"
|
||||
verify_secret
|
||||
test_secret_in_pod
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# Generate new secret key
|
||||
log_step "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}..."
|
||||
echo ""
|
||||
|
||||
# Dry-run mode
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log_info "🔍 Dry-run mode - showing what would be done:"
|
||||
echo ""
|
||||
log_info "Would create secret with the following command:"
|
||||
echo " kubectl create secret generic $SECRET_NAME \\"
|
||||
echo " --from-literal=$SECRET_KEY_NAME='[GENERATED_KEY]' \\"
|
||||
echo " --namespace=$NAMESPACE \\"
|
||||
echo " --dry-run=client -o yaml | kubectl apply -f -"
|
||||
echo ""
|
||||
log_info "Secret key would be: ${SECRET_KEY:0:10}...${SECRET_KEY: -5}"
|
||||
echo ""
|
||||
log_info "Run without --dry-run to execute the deployment"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 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"
|
||||
log_info "✅ Created namespace '$NAMESPACE'"
|
||||
fi
|
||||
|
||||
# Create the secret
|
||||
if create_secret "$SECRET_KEY"; then
|
||||
echo ""
|
||||
# Verify deployment
|
||||
verify_secret
|
||||
echo ""
|
||||
test_secret_in_pod
|
||||
echo ""
|
||||
|
||||
log_info "🎉 Secret deployment completed successfully!"
|
||||
echo ""
|
||||
log_info "📋 Next steps:"
|
||||
log_info "1. Update argocd/deployment.yaml to include environment variable:"
|
||||
echo ""
|
||||
echo " env:"
|
||||
echo " - name: VORGABENUI_SECRET"
|
||||
echo " valueFrom:"
|
||||
echo " secretKeyRef:"
|
||||
echo " name: $SECRET_NAME"
|
||||
echo " key: $SECRET_KEY_NAME"
|
||||
echo ""
|
||||
log_info "2. Apply the updated deployment:"
|
||||
echo " kubectl apply -f argocd/deployment.yaml"
|
||||
echo ""
|
||||
log_info "3. Verify Django pods restart and pick up the new secret"
|
||||
echo ""
|
||||
else
|
||||
log_error "Secret deployment failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
45
scripts/deploy_secret.sh
Executable file
45
scripts/deploy_secret.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
# Generate and deploy Django secret key to Kubernetes
|
||||
|
||||
NAMESPACE="vorgabenui"
|
||||
SECRET_NAME="django-secret"
|
||||
SECRET_FILE="templates/secret.yaml"
|
||||
|
||||
# Check if secret file exists
|
||||
if [ ! -f "$SECRET_FILE" ]; then
|
||||
echo "Error: $SECRET_FILE not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate random secret key
|
||||
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(50))")
|
||||
|
||||
# Create temporary secret file with generated key
|
||||
TEMP_SECRET_FILE=$(mktemp)
|
||||
cat "$SECRET_FILE" | sed "s/CHANGE_ME_TO_RANDOM_STRING/$SECRET_KEY/g" > "$TEMP_SECRET_FILE"
|
||||
|
||||
# Check if secret already exists
|
||||
if kubectl get secret "$SECRET_NAME" -n "$NAMESPACE" &>/dev/null; then
|
||||
echo "Secret $SECRET_NAME already exists in namespace $NAMESPACE"
|
||||
read -p "Do you want to replace it? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "Aborted"
|
||||
rm "$TEMP_SECRET_FILE"
|
||||
exit 0
|
||||
fi
|
||||
kubectl apply -f "$TEMP_SECRET_FILE"
|
||||
echo "Secret updated successfully"
|
||||
else
|
||||
kubectl apply -f "$TEMP_SECRET_FILE"
|
||||
echo "Secret created successfully"
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm "$TEMP_SECRET_FILE"
|
||||
|
||||
echo ""
|
||||
echo "Secret deployed:"
|
||||
echo " Name: $SECRET_NAME"
|
||||
echo " Namespace: $NAMESPACE"
|
||||
echo " Key: secret-key"
|
||||
50
scripts/full_deploy.sh
Executable file
50
scripts/full_deploy.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# Full deployment script - bumps both container versions by 0.001 and copies database
|
||||
|
||||
DEPLOYMENT_FILE="argocd/deployment.yaml"
|
||||
DB_SOURCE="data/db.sqlite3"
|
||||
DB_DEST="data-loader/preload.sqlite3"
|
||||
|
||||
# Check if deployment file exists
|
||||
if [ ! -f "$DEPLOYMENT_FILE" ]; then
|
||||
echo "Error: $DEPLOYMENT_FILE not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if source database exists
|
||||
if [ ! -f "$DB_SOURCE" ]; then
|
||||
echo "Error: $DB_SOURCE not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract current version of data-loader
|
||||
LOADER_VERSION=$(grep -E "image: git.baumann.gr/adebaumann/vgui-data-loader:[0-9]" "$DEPLOYMENT_FILE" | sed -E 's/.*:([0-9.]+)/\1/')
|
||||
|
||||
# Extract current version of main container
|
||||
MAIN_VERSION=$(grep -E "image: git.baumann.gr/adebaumann/vgui:[0-9]" "$DEPLOYMENT_FILE" | grep -v "data-loader" | sed -E 's/.*:([0-9.]+)/\1/')
|
||||
|
||||
if [ -z "$LOADER_VERSION" ] || [ -z "$MAIN_VERSION" ]; then
|
||||
echo "Error: Could not find current versions"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Calculate new versions (add 0.001), preserve leading zero
|
||||
NEW_LOADER_VERSION=$(echo "$LOADER_VERSION + 0.001" | bc | sed 's/^\./0./')
|
||||
NEW_MAIN_VERSION=$(echo "$MAIN_VERSION + 0.001" | bc | sed 's/^\./0./')
|
||||
|
||||
# Update the 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"
|
||||
|
||||
# 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
|
||||
cp "$DB_SOURCE" "$DB_DEST"
|
||||
|
||||
echo "Full deployment prepared:"
|
||||
echo " Data loader: $LOADER_VERSION -> $NEW_LOADER_VERSION"
|
||||
echo " Main container: $MAIN_VERSION -> $NEW_MAIN_VERSION"
|
||||
echo " ConfigMap VERSION: $NEW_MAIN_VERSION"
|
||||
echo " Database copied to $DB_DEST"
|
||||
32
scripts/partial_deploy.sh
Executable file
32
scripts/partial_deploy.sh
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# Partial deployment script - bumps main container version by 0.001
|
||||
|
||||
DEPLOYMENT_FILE="argocd/deployment.yaml"
|
||||
|
||||
# Check if file exists
|
||||
if [ ! -f "$DEPLOYMENT_FILE" ]; then
|
||||
echo "Error: $DEPLOYMENT_FILE not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract current version of main container (labhelper, not labhelper-data-loader)
|
||||
CURRENT_VERSION=$(grep -E "image: git.baumann.gr/adebaumann/vui:[0-9]" "$DEPLOYMENT_FILE" | grep -v "data-loader" | sed -E 's/.*:([0-9.]+)/\1/')
|
||||
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
echo "Error: Could not find current version"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Calculate new version (add 0.001), preserve leading zero
|
||||
NEW_VERSION=$(echo "$CURRENT_VERSION + 0.001" | bc | sed 's/^\./0./')
|
||||
|
||||
# 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"
|
||||
|
||||
# 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 " Main container: $CURRENT_VERSION -> $NEW_VERSION"
|
||||
echo " ConfigMap VERSION: $NEW_VERSION"
|
||||
17
stichworte/migrations/0004_alter_stichwort_options.py
Normal file
17
stichworte/migrations/0004_alter_stichwort_options.py
Normal 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'},
|
||||
),
|
||||
]
|
||||
@@ -9,6 +9,7 @@ class Stichwort(models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural="Stichworte"
|
||||
verbose_name = "Stichwort"
|
||||
|
||||
class Stichworterklaerung (Textabschnitt):
|
||||
erklaerung = models.ForeignKey(Stichwort,on_delete=models.CASCADE)
|
||||
|
||||
28
templates/configmap.yaml
Normal file
28
templates/configmap.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
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/"
|
||||
|
||||
# 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"
|
||||
|
||||
# Performance Configuration
|
||||
# DATA_UPLOAD_MAX_NUMBER_FIELDS: "10250"
|
||||
10
templates/secret.yaml
Normal file
10
templates/secret.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: vorgabenui-secrets
|
||||
namespace: vorgabenui
|
||||
type: Opaque
|
||||
data:
|
||||
# Base64 encoded SECRET_KEY - populated by deployment script
|
||||
# This is a TEMPLATE FILE in templates/ directory - not deployed by ArgoCD
|
||||
vorgabenui_secret: ""
|
||||
Reference in New Issue
Block a user