diff --git a/PASSWORD_SECURITY_ANALYSIS.md b/PASSWORD_SECURITY_ANALYSIS.md
new file mode 100644
index 0000000..f55af23
--- /dev/null
+++ b/PASSWORD_SECURITY_ANALYSIS.md
@@ -0,0 +1,74 @@
+# Passwort-Speicherung Sicherheitsanalyse
+
+## Aktuelle Implementierung
+
+### Wie werden Passwörter gespeichert?
+
+1. **Algorithmus**: `bcrypt` (golang.org/x/crypto/bcrypt)
+2. **Cost Factor**: `bcrypt.DefaultCost` (Wert: **10**)
+3. **Speicherung**:
+ - Feld: `password_hash TEXT NOT NULL` in SQLite
+ - Format: bcrypt Hash-String (enthält automatisch Salt + Hash)
+ - Beispiel: `$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy`
+
+4. **Passwortrichtlinie**:
+ - Mindestens 8 Zeichen
+ - Großbuchstaben erforderlich
+ - Kleinbuchstaben erforderlich
+ - Zahlen erforderlich
+ - Sonderzeichen erforderlich
+
+5. **Validierung**:
+ - Altes Passwort wird bei Änderung geprüft
+ - `bcrypt.CompareHashAndPassword()` für Login-Validierung
+
+## Entspricht es aktuellen Sicherheitsstandards?
+
+### ✅ **Gut implementiert:**
+
+1. **bcrypt ist ein sicherer, bewährter Algorithmus**
+ - Speziell für Passwort-Hashing entwickelt
+ - Verlangsamt Brute-Force-Angriffe durch anpassbare Rechenzeit
+ - Wird von OWASP und anderen Sicherheitsorganisationen empfohlen
+
+2. **Automatisches Salting**
+ - bcrypt generiert für jedes Passwort einen eindeutigen Salt
+ - Verhindert Rainbow-Table-Angriffe
+ - Salt wird im Hash-String mitgespeichert
+
+3. **Passwörter werden nie im Klartext gespeichert**
+ - Nur gehashte Werte in der Datenbank
+ - Einweg-Hashing (nicht reversibel)
+
+4. **Passwortrichtlinie vorhanden**
+ - Erzwingt starke Passwörter
+ - Mindestanforderungen erfüllt
+
+### ⚠️ **Verbesserungspotenzial:**
+
+1. **Cost Factor könnte erhöht werden**
+ - **Aktuell**: Cost 10 (DefaultCost)
+ - **Empfohlen 2024/2025**: Cost 12-14
+ - **Begründung**:
+ - Cost 10 war vor ~10 Jahren Standard
+ - Moderne Hardware ist schneller
+ - Cost 12-14 bietet besseren Schutz gegen Brute-Force
+ - Trade-off: Etwas langsamere Login-Zeit (~100-500ms), aber deutlich sicherer
+
+2. **Fehlende Sicherheitsfeatures** (optional, aber empfohlen):
+ - ❌ Rate Limiting für Login-Versuche (verhindert Brute-Force)
+ - ❌ Passwort-Historie (verhindert Wiederverwendung)
+ - ❌ Passwort-Ablaufzeit
+ - ❌ Account-Lockout nach fehlgeschlagenen Versuchen
+ - ❌ 2FA/MFA Support
+
+## Empfehlung
+
+Die aktuelle Implementierung ist **grundsätzlich sicher** und entspricht **modernen Standards**, aber:
+
+1. **Sofort umsetzbar**: Cost Factor von 10 auf 12-14 erhöhen
+2. **Mittelfristig**: Rate Limiting für Login-Versuche implementieren
+3. **Langfristig**: Zusätzliche Sicherheitsfeatures (2FA, Passwort-Historie)
+
+Soll ich den Cost Factor erhöhen?
+
diff --git a/backend/main.go b/backend/main.go
index c089a11..ff784c8 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -5,9 +5,9 @@ import (
"crypto/x509"
"database/sql"
"encoding/asn1"
+ "encoding/base64"
"encoding/hex"
"encoding/json"
- "encoding/base64"
"encoding/pem"
"fmt"
"io"
@@ -258,25 +258,68 @@ type CSR struct {
// User struct für Benutzer
type User struct {
- ID string `json:"id"`
- Username string `json:"username"`
- Email string `json:"email"`
- CreatedAt string `json:"createdAt"`
+ ID string `json:"id"`
+ Username string `json:"username"`
+ Email string `json:"email"`
+ IsAdmin bool `json:"isAdmin"`
+ Enabled bool `json:"enabled"`
+ CreatedAt string `json:"createdAt"`
+ GroupIDs []string `json:"groupIds,omitempty"`
}
-// CreateUserRequest struct für Benutzer-Erstellung
-type CreateUserRequest struct {
- Username string `json:"username"`
- Email string `json:"email"`
- Password string `json:"password"`
+// PermissionLevel definiert die Berechtigungsstufen
+type PermissionLevel string
+
+const (
+ PermissionRead PermissionLevel = "READ"
+ PermissionReadWrite PermissionLevel = "READ_WRITE"
+ PermissionFullAccess PermissionLevel = "FULL_ACCESS"
+)
+
+// PermissionGroup struct für Berechtigungsgruppen
+type PermissionGroup struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Permission PermissionLevel `json:"permission"`
+ SpaceIDs []string `json:"spaceIds"`
+ CreatedAt string `json:"createdAt"`
+}
+
+// CreatePermissionGroupRequest struct für Gruppen-Erstellung
+type CreatePermissionGroupRequest struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Permission PermissionLevel `json:"permission"`
+ SpaceIDs []string `json:"spaceIds"`
+}
+
+// UpdatePermissionGroupRequest struct für Gruppen-Update
+type UpdatePermissionGroupRequest struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Permission PermissionLevel `json:"permission"`
+ SpaceIDs []string `json:"spaceIds"`
}
// UpdateUserRequest struct für Benutzer-Update
type UpdateUserRequest struct {
- Username string `json:"username,omitempty"`
- Email string `json:"email,omitempty"`
- OldPassword string `json:"oldPassword,omitempty"`
- Password string `json:"password,omitempty"`
+ Username string `json:"username,omitempty"`
+ Email string `json:"email,omitempty"`
+ Password string `json:"password,omitempty"`
+ OldPassword string `json:"oldPassword,omitempty"`
+ IsAdmin *bool `json:"isAdmin,omitempty"`
+ Enabled *bool `json:"enabled,omitempty"`
+ GroupIDs []string `json:"groupIds,omitempty"`
+}
+
+// CreateUserRequest struct für Benutzer-Erstellung
+type CreateUserRequest struct {
+ Username string `json:"username"`
+ Email string `json:"email"`
+ Password string `json:"password"`
+ IsAdmin bool `json:"isAdmin,omitempty"`
+ GroupIDs []string `json:"groupIds,omitempty"`
}
// MessageResponse struct für einfache Nachrichten
@@ -318,7 +361,7 @@ func initDB() {
log.Println("Teste Datenbank-Verbindung...")
maxRetries := 5
for i := 0; i < maxRetries; i++ {
- ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
err := db.PingContext(ctx)
cancel()
if err != nil {
@@ -497,6 +540,8 @@ func initDB() {
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
+ is_admin INTEGER DEFAULT 0,
+ enabled INTEGER DEFAULT 1,
created_at DATETIME NOT NULL
);`
@@ -512,6 +557,22 @@ func initDB() {
log.Println("Datenbank erfolgreich initialisiert")
+ // Füge is_admin Spalte hinzu falls nicht vorhanden
+ ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
+ _, err = db.ExecContext(ctx, "ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0")
+ cancel()
+ if err != nil && !strings.Contains(err.Error(), "duplicate column") {
+ log.Printf("Hinweis: is_admin-Spalte könnte bereits existieren: %v", err)
+ }
+
+ // Füge enabled Spalte hinzu falls nicht vorhanden
+ ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
+ _, err = db.ExecContext(ctx, "ALTER TABLE users ADD COLUMN enabled INTEGER DEFAULT 1")
+ cancel()
+ if err != nil && !strings.Contains(err.Error(), "duplicate column") {
+ log.Printf("Hinweis: enabled-Spalte könnte bereits existieren: %v", err)
+ }
+
// Erstelle Audit-Log-Tabelle
log.Println("Erstelle audit_logs-Tabelle...")
createAuditLogsTableSQL := `
@@ -570,25 +631,97 @@ func initDB() {
} else {
log.Printf("Avatar-Ordner erstellt: %s", avatarDir)
}
+
+ // Erstelle Permission Groups-Tabelle
+ log.Println("Erstelle permission_groups-Tabelle...")
+ createPermissionGroupsTableSQL := `
+ CREATE TABLE IF NOT EXISTS permission_groups (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ description TEXT,
+ permission TEXT NOT NULL,
+ created_at DATETIME NOT NULL
+ );`
+
+ ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
+ _, err = db.ExecContext(ctx, createPermissionGroupsTableSQL)
+ cancel()
+ if err != nil {
+ if strings.Contains(err.Error(), "database is locked") {
+ log.Fatal("datenbank ist gesperrt. Bitte beenden Sie alle anderen Prozesse, die die Datenbank verwenden.")
+ }
+ log.Fatal("Fehler beim Erstellen der permission_groups-Tabelle:", err)
+ }
+
+ // Erstelle group_spaces-Tabelle für Space-Zuweisungen
+ log.Println("Erstelle group_spaces-Tabelle...")
+ createGroupSpacesTableSQL := `
+ CREATE TABLE IF NOT EXISTS group_spaces (
+ group_id TEXT NOT NULL,
+ space_id TEXT NOT NULL,
+ PRIMARY KEY (group_id, space_id),
+ FOREIGN KEY (group_id) REFERENCES permission_groups(id) ON DELETE CASCADE,
+ FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE
+ );`
+
+ ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
+ _, err = db.ExecContext(ctx, createGroupSpacesTableSQL)
+ cancel()
+ if err != nil {
+ if strings.Contains(err.Error(), "database is locked") {
+ log.Fatal("datenbank ist gesperrt. Bitte beenden Sie alle anderen Prozesse, die die Datenbank verwenden.")
+ }
+ log.Fatal("Fehler beim Erstellen der group_spaces-Tabelle:", err)
+ }
+
+ // Erstelle user_groups-Tabelle für Benutzer-Gruppen-Zuweisungen
+ log.Println("Erstelle user_groups-Tabelle...")
+ createUserGroupsTableSQL := `
+ CREATE TABLE IF NOT EXISTS user_groups (
+ user_id TEXT NOT NULL,
+ group_id TEXT NOT NULL,
+ PRIMARY KEY (user_id, group_id),
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (group_id) REFERENCES permission_groups(id) ON DELETE CASCADE
+ );`
+
+ ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
+ _, err = db.ExecContext(ctx, createUserGroupsTableSQL)
+ cancel()
+ if err != nil {
+ if strings.Contains(err.Error(), "database is locked") {
+ log.Fatal("datenbank ist gesperrt. Bitte beenden Sie alle anderen Prozesse, die die Datenbank verwenden.")
+ }
+ log.Fatal("Fehler beim Erstellen der user_groups-Tabelle:", err)
+ }
+
+ log.Println("Berechtigungssystem-Tabellen erfolgreich erstellt")
}
func createDefaultAdmin() {
- ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
- // Prüfe ob bereits ein Admin-User existiert
+ // Prüfe ob bereits ein Admin-User mit UID "admin" existiert
var count int
- err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = 'admin'").Scan(&count)
+ err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE id = 'admin'").Scan(&count)
if err != nil {
log.Printf("Fehler beim Prüfen des Admin-Users: %v", err)
return
}
if count > 0 {
- log.Println("Admin-User existiert bereits")
+ log.Println("Admin-User mit UID 'admin' existiert bereits")
+ // Stelle sicher, dass der Admin-User als Admin markiert ist
+ _, err = db.ExecContext(ctx, "UPDATE users SET is_admin = 1 WHERE id = 'admin'")
+ if err != nil {
+ log.Printf("Warnung: Konnte Admin-Status nicht setzen: %v", err)
+ } else {
+ log.Println("Admin-User ist als Administrator markiert")
+ }
// Prüfe ob das Passwort noch "admin" ist (für Debugging)
var storedHash string
- err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE username = 'admin'").Scan(&storedHash)
+ err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE id = 'admin'").Scan(&storedHash)
if err == nil {
// Teste ob das Passwort "admin" ist
testErr := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte("admin"))
@@ -601,7 +734,45 @@ func createDefaultAdmin() {
return
}
- // Erstelle Default Admin-User
+ // Migration: Falls ein Admin-User mit username='admin' aber anderer UID existiert, migriere ihn
+ var existingAdminID string
+ err = db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = 'admin' AND id != 'admin' LIMIT 1").Scan(&existingAdminID)
+ if err == nil {
+ log.Printf("Migriere bestehenden Admin-User von UID '%s' zu UID 'admin'", existingAdminID)
+ // Hole alle Daten des alten Admin-Users
+ var oldUsername, oldEmail, oldPasswordHash string
+ var oldIsAdmin, oldEnabled int
+ var oldCreatedAt string
+ err = db.QueryRowContext(ctx, "SELECT username, email, password_hash, is_admin, enabled, created_at FROM users WHERE id = ?", existingAdminID).
+ Scan(&oldUsername, &oldEmail, &oldPasswordHash, &oldIsAdmin, &oldEnabled, &oldCreatedAt)
+ if err == nil {
+ // Erstelle neuen Admin-User mit UID 'admin'
+ _, err = db.ExecContext(ctx,
+ "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ "admin", oldUsername, oldEmail, oldPasswordHash, oldIsAdmin, oldEnabled, oldCreatedAt)
+ if err == nil {
+ // Migriere user_groups Zuweisungen
+ _, err = db.ExecContext(ctx, "UPDATE user_groups SET user_id = 'admin' WHERE user_id = ?", existingAdminID)
+ if err != nil {
+ log.Printf("Warnung: Konnte user_groups nicht migrieren: %v", err)
+ }
+ // Lösche den alten User (CASCADE sollte user_groups automatisch löschen)
+ _, err = db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", existingAdminID)
+ if err == nil {
+ log.Printf("✓ Admin-User erfolgreich zu UID 'admin' migriert")
+ return
+ } else {
+ log.Printf("Warnung: Konnte alten Admin-User nicht löschen: %v", err)
+ }
+ } else {
+ log.Printf("Warnung: Konnte neuen Admin-User nicht erstellen: %v", err)
+ }
+ } else {
+ log.Printf("Warnung: Konnte Daten des alten Admin-Users nicht lesen: %v", err)
+ }
+ }
+
+ // Erstelle Default Admin-User mit fester UID "admin"
adminPassword := "admin" // Default Passwort - sollte in Produktion geändert werden
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
if err != nil {
@@ -609,18 +780,18 @@ func createDefaultAdmin() {
return
}
- adminID := uuid.New().String()
+ adminID := "admin" // Feste UID statt UUID
createdAt := time.Now().Format(time.RFC3339)
_, err = db.ExecContext(ctx,
- "INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)",
- adminID, "admin", "admin@certigo.local", string(hashedPassword), createdAt)
+ "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ adminID, "admin", "admin@certigo.local", string(hashedPassword), 1, 1, createdAt)
if err != nil {
log.Printf("Fehler beim Erstellen des Admin-Users: %v", err)
return
}
- log.Println("✓ Default Admin-User erstellt: username='admin', password='admin'")
+ log.Println("✓ Default Admin-User erstellt: UID='admin', username='admin', password='admin'")
log.Printf(" User ID: %s", adminID)
log.Printf(" Email: admin@certigo.local")
}
@@ -699,11 +870,11 @@ func getStatsHandler(w http.ResponseWriter, r *http.Request) {
}
response := StatsResponse{
- Spaces: spacesCount,
- FQDNs: fqdnsCount,
- CSRs: csrsCount,
+ Spaces: spacesCount,
+ FQDNs: fqdnsCount,
+ CSRs: csrsCount,
Certificates: certificatesCount,
- Users: usersCount,
+ Users: usersCount,
}
json.NewEncoder(w).Encode(response)
@@ -720,16 +891,35 @@ func getSpacesHandler(w http.ResponseWriter, r *http.Request) {
return
}
- // Verwende Prepared Statement für bessere Performance und Sicherheit
- stmt, err := db.Prepare("SELECT id, name, description, created_at FROM spaces ORDER BY created_at DESC")
+ // Hole Benutzer-ID
+ userID, _ := getUserFromRequest(r)
+
+ // Hole alle Spaces, auf die der Benutzer Zugriff hat
+ accessibleSpaceIDs, err := getAccessibleSpaceIDs(userID)
if err != nil {
- http.Error(w, "Fehler beim Vorbereiten der Abfrage", http.StatusInternalServerError)
- log.Printf("Fehler beim Vorbereiten der Abfrage: %v", err)
+ http.Error(w, "Fehler beim Prüfen der Berechtigungen", http.StatusInternalServerError)
+ log.Printf("Fehler beim Prüfen der Berechtigungen: %v", err)
return
}
- defer stmt.Close()
- rows, err := stmt.Query()
+ // Wenn der Benutzer keinen Zugriff auf Spaces hat, gebe leeres Array zurück
+ if len(accessibleSpaceIDs) == 0 {
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode([]Space{})
+ return
+ }
+
+ // Baue Query mit IN-Klausel für die zugänglichen Spaces
+ placeholders := make([]string, len(accessibleSpaceIDs))
+ args := make([]interface{}, len(accessibleSpaceIDs))
+ for i, spaceID := range accessibleSpaceIDs {
+ placeholders[i] = "?"
+ args[i] = spaceID
+ }
+
+ query := fmt.Sprintf("SELECT id, name, description, created_at FROM spaces WHERE id IN (%s) ORDER BY created_at DESC", strings.Join(placeholders, ","))
+
+ rows, err := db.Query(query, args...)
if err != nil {
http.Error(w, "Fehler beim Abrufen der Spaces", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen der Spaces: %v", err)
@@ -785,6 +975,47 @@ func createSpaceHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prüfe Berechtigung: Nur FULL_ACCESS darf Spaces erstellen
+ userID, username := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ // Prüfe ob User Admin ist - Admins haben immer Vollzugriff
+ isAdmin, err := isUserAdmin(userID)
+ if err != nil {
+ log.Printf("Fehler beim Prüfen des Admin-Status: %v", err)
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ return
+ }
+
+ // Prüfe, ob der Benutzer FULL_ACCESS hat (ohne Space-Beschränkung)
+ permissions, err := getUserPermissions(userID)
+ if err != nil {
+ http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError)
+ log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err)
+ return
+ }
+
+ // Admin oder HasFullAccess erlaubt Space-Erstellung
+ hasFullAccess := isAdmin || permissions.HasFullAccess
+
+ // Wenn nicht Admin, prüfe auch Gruppen
+ if !isAdmin && len(permissions.Groups) > 0 {
+ for _, group := range permissions.Groups {
+ if group.Permission == PermissionFullAccess {
+ hasFullAccess = true
+ break
+ }
+ }
+ }
+
+ if !hasFullAccess {
+ http.Error(w, "Keine Berechtigung zum Erstellen von Spaces. Vollzugriff erforderlich.", http.StatusForbidden)
+ return
+ }
+
var req CreateSpaceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
@@ -801,7 +1032,7 @@ func createSpaceHandler(w http.ResponseWriter, r *http.Request) {
createdAt := time.Now()
// Speichere in Datenbank
- _, err := db.Exec(
+ _, err = db.Exec(
"INSERT INTO spaces (id, name, description, created_at) VALUES (?, ?, ?, ?)",
id, req.Name, req.Description, createdAt,
)
@@ -823,7 +1054,6 @@ func createSpaceHandler(w http.ResponseWriter, r *http.Request) {
// Audit-Log: Space erstellt
if auditService != nil {
- userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "CREATE", "space", id, userID, username, map[string]interface{}{
"name": req.Name,
@@ -852,9 +1082,28 @@ func deleteSpaceHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prüfe Berechtigung: Nur FULL_ACCESS darf Spaces löschen
+ userID, username := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ hasPermission, err := hasPermission(userID, id, PermissionFullAccess)
+ if err != nil {
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ log.Printf("Fehler beim Prüfen der Berechtigung: %v", err)
+ return
+ }
+
+ if !hasPermission {
+ http.Error(w, "Keine Berechtigung zum Löschen von Spaces. Vollzugriff erforderlich.", http.StatusForbidden)
+ return
+ }
+
// Prüfe ob der Space existiert
var exists bool
- err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", id).Scan(&exists)
+ err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", id).Scan(&exists)
if err != nil {
http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError)
log.Printf("Fehler beim Prüfen des Space: %v", err)
@@ -937,7 +1186,6 @@ func deleteSpaceHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"message": "Space erfolgreich gelöscht"})
// Audit-Log: Space gelöscht
- userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
details := map[string]interface{}{
"message": fmt.Sprintf("Space gelöscht: %s", id),
@@ -968,8 +1216,27 @@ func getSpaceFqdnCountHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prüfe Berechtigung: Benutzer muss Zugriff auf den Space haben
+ userID, _ := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ hasAccess, err := hasSpaceAccess(userID, spaceID)
+ if err != nil {
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ log.Printf("Fehler beim Prüfen der Berechtigung: %v", err)
+ return
+ }
+
+ if !hasAccess {
+ http.Error(w, "Keine Berechtigung für diesen Space", http.StatusForbidden)
+ return
+ }
+
var count int
- err := db.QueryRow("SELECT COUNT(*) FROM fqdns WHERE space_id = ?", spaceID).Scan(&count)
+ err = db.QueryRow("SELECT COUNT(*) FROM fqdns WHERE space_id = ?", spaceID).Scan(&count)
if err != nil {
http.Error(w, "Fehler beim Abrufen der FQDN-Anzahl", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen der FQDN-Anzahl: %v", err)
@@ -998,9 +1265,28 @@ func getFqdnsHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prüfe Berechtigung: Benutzer muss Zugriff auf den Space haben
+ userID, _ := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ hasAccess, err := hasSpaceAccess(userID, spaceID)
+ if err != nil {
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ log.Printf("Fehler beim Prüfen der Berechtigung: %v", err)
+ return
+ }
+
+ if !hasAccess {
+ http.Error(w, "Keine Berechtigung für diesen Space", http.StatusForbidden)
+ return
+ }
+
// Prüfe ob der Space existiert
var exists bool
- err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists)
+ err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists)
if err != nil {
http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError)
log.Printf("Fehler beim Prüfen des Space: %v", err)
@@ -1072,9 +1358,28 @@ func createFqdnHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prüfe Berechtigung: READ_WRITE oder FULL_ACCESS erforderlich zum Erstellen von FQDNs
+ userID, username := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ hasPermission, err := hasPermission(userID, spaceID, PermissionReadWrite)
+ if err != nil {
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ log.Printf("Fehler beim Prüfen der Berechtigung: %v", err)
+ return
+ }
+
+ if !hasPermission {
+ http.Error(w, "Keine Berechtigung zum Erstellen von FQDNs. Lesen/Schreiben oder Vollzugriff erforderlich.", http.StatusForbidden)
+ return
+ }
+
// Prüfe ob der Space existiert
var exists bool
- err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists)
+ err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists)
if err != nil {
http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError)
log.Printf("Fehler beim Prüfen des Space: %v", err)
@@ -1138,7 +1443,6 @@ func createFqdnHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(newFqdn)
// Audit-Log: FQDN erstellt
- userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "CREATE", "fqdn", id, userID, username, map[string]interface{}{
"fqdn": req.FQDN,
@@ -1168,9 +1472,28 @@ func deleteFqdnHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prüfe Berechtigung: Nur FULL_ACCESS darf FQDNs löschen
+ userID, username := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ hasPermission, err := hasPermission(userID, spaceID, PermissionFullAccess)
+ if err != nil {
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ log.Printf("Fehler beim Prüfen der Berechtigung: %v", err)
+ return
+ }
+
+ if !hasPermission {
+ http.Error(w, "Keine Berechtigung zum Löschen von FQDNs. Vollzugriff erforderlich.", http.StatusForbidden)
+ return
+ }
+
// Prüfe ob der FQDN existiert und zum Space gehört
var exists bool
- err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM fqdns WHERE id = ? AND space_id = ?)", fqdnID, spaceID).Scan(&exists)
+ err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM fqdns WHERE id = ? AND space_id = ?)", fqdnID, spaceID).Scan(&exists)
if err != nil {
http.Error(w, "Fehler beim Prüfen des FQDN", http.StatusInternalServerError)
log.Printf("Fehler beim Prüfen des FQDN: %v", err)
@@ -1234,7 +1557,6 @@ func deleteFqdnHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"message": "FQDN erfolgreich gelöscht"})
// Audit-Log: FQDN gelöscht
- userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "DELETE", "fqdn", fqdnID, userID, username, map[string]interface{}{
"spaceId": spaceID,
@@ -1261,9 +1583,28 @@ func deleteAllFqdnsHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prüfe Berechtigung: Nur FULL_ACCESS darf alle FQDNs eines Spaces löschen
+ userID, _ := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ hasPermission, err := hasPermission(userID, spaceID, PermissionFullAccess)
+ if err != nil {
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ log.Printf("Fehler beim Prüfen der Berechtigung: %v", err)
+ return
+ }
+
+ if !hasPermission {
+ http.Error(w, "Keine Berechtigung zum Löschen aller FQDNs. Vollzugriff erforderlich.", http.StatusForbidden)
+ return
+ }
+
// Prüfe ob der Space existiert
var exists bool
- err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists)
+ err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists)
if err != nil {
http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError)
log.Printf("Fehler beim Prüfen des Space: %v", err)
@@ -1335,6 +1676,46 @@ func deleteAllFqdnsGlobalHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prüfe Berechtigung: Nur FULL_ACCESS darf alle FQDNs global löschen
+ userID, _ := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ // Prüfe ob User Admin ist - Admins haben immer Vollzugriff
+ isAdmin, err := isUserAdmin(userID)
+ if err != nil {
+ log.Printf("Fehler beim Prüfen des Admin-Status: %v", err)
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ return
+ }
+
+ permissions, err := getUserPermissions(userID)
+ if err != nil {
+ http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError)
+ log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err)
+ return
+ }
+
+ // Admin oder HasFullAccess erlaubt Löschen aller FQDNs
+ hasFullAccess := isAdmin || permissions.HasFullAccess
+
+ // Wenn nicht Admin, prüfe auch Gruppen
+ if !isAdmin && len(permissions.Groups) > 0 {
+ for _, group := range permissions.Groups {
+ if group.Permission == PermissionFullAccess {
+ hasFullAccess = true
+ break
+ }
+ }
+ }
+
+ if !hasFullAccess {
+ http.Error(w, "Keine Berechtigung zum Löschen aller FQDNs. Vollzugriff erforderlich.", http.StatusForbidden)
+ return
+ }
+
// Prüfe Query-Parameter für Bestätigung (Sicherheitsmaßnahme)
confirm := r.URL.Query().Get("confirm")
if confirm != "true" {
@@ -1344,7 +1725,7 @@ func deleteAllFqdnsGlobalHandler(w http.ResponseWriter, r *http.Request) {
// Zähle zuerst die Anzahl der FQDNs
var totalCount int
- err := db.QueryRow("SELECT COUNT(*) FROM fqdns").Scan(&totalCount)
+ err = db.QueryRow("SELECT COUNT(*) FROM fqdns").Scan(&totalCount)
if err != nil {
http.Error(w, "Fehler beim Zählen der FQDNs", http.StatusInternalServerError)
log.Printf("Fehler beim Zählen der FQDNs: %v", err)
@@ -1420,6 +1801,46 @@ func deleteAllCSRsHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prüfe Berechtigung: Nur FULL_ACCESS darf alle CSRs löschen
+ userID, _ := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ // Prüfe ob User Admin ist - Admins haben immer Vollzugriff
+ isAdmin, err := isUserAdmin(userID)
+ if err != nil {
+ log.Printf("Fehler beim Prüfen des Admin-Status: %v", err)
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ return
+ }
+
+ permissions, err := getUserPermissions(userID)
+ if err != nil {
+ http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError)
+ log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err)
+ return
+ }
+
+ // Admin oder HasFullAccess erlaubt Löschen aller CSRs
+ hasFullAccess := isAdmin || permissions.HasFullAccess
+
+ // Wenn nicht Admin, prüfe auch Gruppen
+ if !isAdmin && len(permissions.Groups) > 0 {
+ for _, group := range permissions.Groups {
+ if group.Permission == PermissionFullAccess {
+ hasFullAccess = true
+ break
+ }
+ }
+ }
+
+ if !hasFullAccess {
+ http.Error(w, "Keine Berechtigung zum Löschen aller CSRs. Vollzugriff erforderlich.", http.StatusForbidden)
+ return
+ }
+
// Prüfe Query-Parameter für Bestätigung (Sicherheitsmaßnahme)
confirm := r.URL.Query().Get("confirm")
if confirm != "true" {
@@ -1429,7 +1850,7 @@ func deleteAllCSRsHandler(w http.ResponseWriter, r *http.Request) {
// Zähle zuerst die Anzahl der CSRs
var totalCount int
- err := db.QueryRow("SELECT COUNT(*) FROM csrs").Scan(&totalCount)
+ err = db.QueryRow("SELECT COUNT(*) FROM csrs").Scan(&totalCount)
if err != nil {
http.Error(w, "Fehler beim Zählen der CSRs", http.StatusInternalServerError)
log.Printf("Fehler beim Zählen der CSRs: %v", err)
@@ -1512,6 +1933,25 @@ func uploadCSRHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prüfe Berechtigung: READ_WRITE oder FULL_ACCESS erforderlich zum Hochladen von CSRs
+ userID, username := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ hasPermission, err := hasPermission(userID, spaceID, PermissionReadWrite)
+ if err != nil {
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ log.Printf("Fehler beim Prüfen der Berechtigung: %v", err)
+ return
+ }
+
+ if !hasPermission {
+ http.Error(w, "Keine Berechtigung zum Hochladen von CSRs. Lesen/Schreiben oder Vollzugriff erforderlich.", http.StatusForbidden)
+ return
+ }
+
// Prüfe ob Space existiert
var spaceExists bool
err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&spaceExists)
@@ -1658,7 +2098,6 @@ func uploadCSRHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(newCSR)
// Audit-Log: CSR hochgeladen
- userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "UPLOAD", "csr", csrID, userID, username, map[string]interface{}{
"fqdnId": fqdnID,
@@ -1687,6 +2126,25 @@ func getCSRByFQDNHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prüfe Berechtigung: Benutzer muss Zugriff auf den Space haben (READ, READ_WRITE oder FULL_ACCESS)
+ userID, _ := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ hasAccess, err := hasSpaceAccess(userID, spaceID)
+ if err != nil {
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ log.Printf("Fehler beim Prüfen der Berechtigung: %v", err)
+ return
+ }
+
+ if !hasAccess {
+ http.Error(w, "Keine Berechtigung für diesen Space", http.StatusForbidden)
+ return
+ }
+
// Prüfe ob nur der neueste CSR gewünscht ist
latestOnly := r.URL.Query().Get("latest") == "true"
@@ -2399,6 +2857,150 @@ components:
// User Handler Functions
+func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
+
+ if r.Method == "OPTIONS" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ // Hole Benutzer-ID
+ userID, _ := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ // Prüfe ob User Admin ist
+ isAdmin, err := isUserAdmin(userID)
+ if err != nil {
+ log.Printf("Fehler beim Prüfen des Admin-Status: %v", err)
+ isAdmin = false
+ }
+
+ // Hole Berechtigungen
+ permissions, err := getUserPermissions(userID)
+ if err != nil {
+ http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError)
+ log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err)
+ return
+ }
+
+ // Erstelle vereinfachte Antwort für Frontend
+ canCreateFqdn := make(map[string]bool)
+ canDeleteFqdn := make(map[string]bool)
+ canUploadCSR := make(map[string]bool)
+ canSignCSR := make(map[string]bool)
+
+ response := map[string]interface{}{
+ "userId": userID,
+ "isAdmin": isAdmin,
+ "hasFullAccess": permissions.HasFullAccess || isAdmin,
+ "accessibleSpaces": []string{},
+ "permissions": map[string]interface{}{
+ "canCreateSpace": permissions.HasFullAccess || isAdmin,
+ "canDeleteSpace": permissions.HasFullAccess || isAdmin,
+ "canCreateFqdn": canCreateFqdn,
+ "canDeleteFqdn": canDeleteFqdn,
+ "canUploadCSR": canUploadCSR,
+ "canSignCSR": canSignCSR,
+ },
+ }
+
+ // Hole alle Spaces
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+ defer cancel()
+
+ spaceRows, err := db.QueryContext(ctx, "SELECT id FROM spaces")
+ if err == nil {
+ defer spaceRows.Close()
+ var allSpaceIDs []string
+ for spaceRows.Next() {
+ var spaceID string
+ if err := spaceRows.Scan(&spaceID); err == nil {
+ allSpaceIDs = append(allSpaceIDs, spaceID)
+ }
+ }
+ spaceRows.Close()
+
+ // Prüfe für jeden Space die Berechtigungen
+ accessibleSpaces := []string{}
+
+ for _, spaceID := range allSpaceIDs {
+ hasAccess, _ := hasSpaceAccess(userID, spaceID)
+ if hasAccess {
+ accessibleSpaces = append(accessibleSpaces, spaceID)
+
+ // Prüfe READ_WRITE für FQDN erstellen und CSR upload/sign
+ hasReadWrite, _ := hasPermission(userID, spaceID, PermissionReadWrite)
+ canCreateFqdn[spaceID] = hasReadWrite
+ canUploadCSR[spaceID] = hasReadWrite
+ canSignCSR[spaceID] = hasReadWrite
+
+ // Prüfe FULL_ACCESS für FQDN löschen
+ hasFullAccess, _ := hasPermission(userID, spaceID, PermissionFullAccess)
+ canDeleteFqdn[spaceID] = hasFullAccess
+ }
+ }
+
+ response["accessibleSpaces"] = accessibleSpaces
+ perms := response["permissions"].(map[string]interface{})
+ perms["canCreateFqdn"] = canCreateFqdn
+ perms["canDeleteFqdn"] = canDeleteFqdn
+ perms["canUploadCSR"] = canUploadCSR
+ perms["canSignCSR"] = canSignCSR
+ }
+
+ // Prüfe globale Berechtigungen (Space erstellen/löschen)
+ // Admins haben immer Vollzugriff
+ if isAdmin {
+ perms := response["permissions"].(map[string]interface{})
+ perms["canCreateSpace"] = true
+ perms["canDeleteSpace"] = true
+ // Alle Spaces sind zugänglich für Admins
+ spaceRows, err := db.QueryContext(ctx, "SELECT id FROM spaces")
+ if err == nil {
+ defer spaceRows.Close()
+ var allSpaceIDs []string
+ for spaceRows.Next() {
+ var spaceID string
+ if err := spaceRows.Scan(&spaceID); err == nil {
+ allSpaceIDs = append(allSpaceIDs, spaceID)
+ canCreateFqdn[spaceID] = true
+ canDeleteFqdn[spaceID] = true
+ canUploadCSR[spaceID] = true
+ canSignCSR[spaceID] = true
+ }
+ }
+ spaceRows.Close()
+ response["accessibleSpaces"] = allSpaceIDs
+ perms["canCreateFqdn"] = canCreateFqdn
+ perms["canDeleteFqdn"] = canDeleteFqdn
+ perms["canUploadCSR"] = canUploadCSR
+ perms["canSignCSR"] = canSignCSR
+ }
+ } else {
+ hasFullAccessGlobal := false
+ for _, group := range permissions.Groups {
+ if group.Permission == PermissionFullAccess {
+ hasFullAccessGlobal = true
+ break
+ }
+ }
+
+ perms := response["permissions"].(map[string]interface{})
+ perms["canCreateSpace"] = hasFullAccessGlobal
+ perms["canDeleteSpace"] = hasFullAccessGlobal
+ }
+
+ w.WriteHeader(http.StatusOK)
+ json.NewEncoder(w).Encode(response)
+}
+
func getUsersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
@@ -2410,10 +3012,11 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) {
return
}
- ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
- rows, err := db.QueryContext(ctx, "SELECT id, username, email, created_at FROM users ORDER BY created_at DESC")
+ // Lade alle Benutzer
+ rows, err := db.QueryContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users ORDER BY created_at DESC")
if err != nil {
http.Error(w, "Fehler beim Abrufen der Benutzer", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen der Benutzer: %v", err)
@@ -2422,15 +3025,54 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) {
defer rows.Close()
var users []User
+ var userIDs []string
for rows.Next() {
var user User
- err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
+ var isAdmin, enabled int
+ err := rows.Scan(&user.ID, &user.Username, &user.Email, &isAdmin, &enabled, &user.CreatedAt)
if err != nil {
http.Error(w, "Fehler beim Lesen der Benutzerdaten", http.StatusInternalServerError)
log.Printf("Fehler beim Lesen der Benutzerdaten: %v", err)
return
}
+ user.IsAdmin = isAdmin == 1
+ user.Enabled = enabled == 1
+ user.GroupIDs = []string{} // Initialisiere als leeres Array
users = append(users, user)
+ userIDs = append(userIDs, user.ID)
+ }
+ rows.Close()
+
+ // Lade alle Gruppen-Zuweisungen in einem einzigen Query
+ if len(userIDs) > 0 {
+ // Erstelle Platzhalter für IN-Clause
+ placeholders := make([]string, len(userIDs))
+ args := make([]interface{}, len(userIDs))
+ for i, id := range userIDs {
+ placeholders[i] = "?"
+ args[i] = id
+ }
+
+ query := fmt.Sprintf("SELECT user_id, group_id FROM user_groups WHERE user_id IN (%s)", strings.Join(placeholders, ","))
+ groupRows, err := db.QueryContext(ctx, query, args...)
+ if err == nil {
+ // Erstelle eine Map von user_id zu group_ids
+ groupMap := make(map[string][]string)
+ for groupRows.Next() {
+ var userID, groupID string
+ if err := groupRows.Scan(&userID, &groupID); err == nil {
+ groupMap[userID] = append(groupMap[userID], groupID)
+ }
+ }
+ groupRows.Close()
+
+ // Weise Gruppen-IDs den Benutzern zu
+ for i := range users {
+ if groupIDs, exists := groupMap[users[i].ID]; exists {
+ users[i].GroupIDs = groupIDs
+ }
+ }
+ }
}
json.NewEncoder(w).Encode(users)
@@ -2450,12 +3092,13 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userID := vars["id"]
- ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
var user User
- err := db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID).
- Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
+ var isAdmin, enabled int
+ err := db.QueryRowContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users WHERE id = ?", userID).
+ Scan(&user.ID, &user.Username, &user.Email, &isAdmin, &enabled, &user.CreatedAt)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
@@ -2465,6 +3108,24 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Fehler beim Abrufen des Benutzers: %v", err)
return
}
+ user.IsAdmin = isAdmin == 1
+ user.Enabled = enabled == 1
+
+ // Lade Gruppen-IDs für diesen Benutzer
+ groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID)
+ if err == nil {
+ var groupIDs []string
+ for groupRows.Next() {
+ var groupID string
+ if err := groupRows.Scan(&groupID); err == nil {
+ groupIDs = append(groupIDs, groupID)
+ }
+ }
+ groupRows.Close()
+ user.GroupIDs = groupIDs
+ } else {
+ user.GroupIDs = []string{} // Initialisiere als leeres Array bei Fehler
+ }
json.NewEncoder(w).Encode(user)
}
@@ -2511,12 +3172,21 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
userID := uuid.New().String()
createdAt := time.Now().Format(time.RFC3339)
- ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
+ isAdmin := 0
+ if req.IsAdmin {
+ isAdmin = 1
+ }
+ // Admin muss immer enabled sein
+ enabledValue := 1
+ if req.IsAdmin {
+ enabledValue = 1 // Admin immer enabled
+ }
_, err = db.ExecContext(ctx,
- "INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)",
- userID, req.Username, req.Email, string(hashedPassword), createdAt)
+ "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ userID, req.Username, req.Email, string(hashedPassword), isAdmin, enabledValue, createdAt)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
if strings.Contains(err.Error(), "username") {
@@ -2531,11 +3201,29 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Weise Gruppen zu, falls angegeben
+ if len(req.GroupIDs) > 0 {
+ for _, groupID := range req.GroupIDs {
+ // Prüfe ob Gruppe existiert
+ var exists bool
+ err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM permission_groups WHERE id = ?)", groupID).Scan(&exists)
+ if err == nil && exists {
+ _, err = db.ExecContext(ctx, "INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?, ?)", userID, groupID)
+ if err != nil {
+ log.Printf("Fehler beim Zuweisen der Gruppe %s zum Benutzer: %v", groupID, err)
+ }
+ }
+ }
+ }
+
user := User{
ID: userID,
Username: req.Username,
Email: req.Email,
+ IsAdmin: req.IsAdmin,
+ Enabled: true,
CreatedAt: createdAt,
+ GroupIDs: req.GroupIDs,
}
w.WriteHeader(http.StatusCreated)
@@ -2544,11 +3232,17 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
// Audit-Log: User erstellt
requestUserID, requestUsername := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
- auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, map[string]interface{}{
+ auditDetails := map[string]interface{}{
"username": req.Username,
"email": req.Email,
+ "groupIds": req.GroupIDs,
"message": fmt.Sprintf("User erstellt: %s (%s)", req.Username, req.Email),
- }, ipAddress, userAgent)
+ }
+ if req.IsAdmin {
+ auditDetails["isAdmin"] = true
+ auditDetails["message"] = fmt.Sprintf("Administrator erstellt: %s (%s)", req.Username, req.Email)
+ }
+ auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, auditDetails, ipAddress, userAgent)
}
func updateUserHandler(w http.ResponseWriter, r *http.Request) {
@@ -2571,29 +3265,109 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
return
}
- ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// Prüfe ob Benutzer existiert
- var exists bool
- err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE id = ?)", userID).Scan(&exists)
- if err != nil || !exists {
- http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
+ var isAdmin int
+ var currentUsername, currentEmail string
+ err := db.QueryRowContext(ctx, "SELECT is_admin, username, email FROM users WHERE id = ?", userID).
+ Scan(&isAdmin, ¤tUsername, ¤tEmail)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
+ return
+ }
+ http.Error(w, "Fehler beim Abrufen des Benutzers", http.StatusInternalServerError)
+ log.Printf("Fehler beim Abrufen des Benutzers: %v", err)
return
}
+ // Nur der spezielle Admin-User mit UID "admin": Username und Email sind unveränderbar
+ // Andere Admin-User können ihre Daten ändern
+ if userID == "admin" {
+ if req.Username != "" && req.Username != currentUsername {
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden"})
+ return
+ }
+ if req.Email != "" && req.Email != currentEmail {
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden"})
+ return
+ }
+ }
+
// Update Felder
updates := []string{}
args := []interface{}{}
- if req.Username != "" {
+ // Nur Username/Email updaten wenn nicht der spezielle Admin-User mit UID "admin"
+ // Andere Admin-User können ihre Daten ändern
+ if req.Username != "" && (userID != "admin" || req.Username == currentUsername) {
updates = append(updates, "username = ?")
args = append(args, req.Username)
}
- if req.Email != "" {
+ if req.Email != "" && (userID != "admin" || req.Email == currentEmail) {
updates = append(updates, "email = ?")
args = append(args, req.Email)
}
+
+ // isAdmin aktualisieren, falls angegeben
+ // UID 'admin' kann seinen Admin-Status nicht ändern
+ if req.IsAdmin != nil {
+ // UID 'admin' ist immer Admin und kann nicht geändert werden
+ if userID == "admin" {
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Der Admin-Status des Users mit UID 'admin' kann nicht geändert werden"})
+ return
+ }
+
+ adminValue := 0
+ if *req.IsAdmin {
+ adminValue = 1
+ }
+ updates = append(updates, "is_admin = ?")
+ args = append(args, adminValue)
+
+ // Wenn Admin aktiviert wird, entferne alle Gruppen-Zuweisungen
+ if *req.IsAdmin {
+ _, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID)
+ if err != nil {
+ log.Printf("Fehler beim Löschen der Gruppen-Zuweisungen für Admin: %v", err)
+ }
+ }
+ }
+
+ // enabled aktualisieren, falls angegeben
+ // Nur der spezielle Admin-User mit UID "admin" kann enabled geändert werden
+ if req.Enabled != nil {
+ // Nur UID "admin" kann enabled geändert werden
+ if userID != "admin" {
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Nur der Admin-User mit UID 'admin' kann aktiviert/deaktiviert werden"})
+ return
+ }
+
+ // Prüfe ob der anfragende User ein Admin ist (für Deaktivierung)
+ if !*req.Enabled {
+ requestUserID, _ := getUserFromRequest(r)
+ isRequestingAdmin, err := isUserAdmin(requestUserID)
+ if err != nil || !isRequestingAdmin {
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Nur Administratoren können den Admin-User mit UID 'admin' deaktivieren"})
+ return
+ }
+ }
+
+ enabledValue := 0
+ if *req.Enabled {
+ enabledValue = 1
+ }
+ updates = append(updates, "enabled = ?")
+ args = append(args, enabledValue)
+ }
+
if req.Password != "" {
// Altes Passwort ist erforderlich, wenn Passwort geändert wird
if req.OldPassword == "" {
@@ -2659,20 +3433,81 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Aktualisiere Gruppen-Zuweisungen, falls angegeben
+ // Nur wenn User nicht Admin ist oder Admin deaktiviert wird
+ if req.GroupIDs != nil {
+ // Prüfe ob User nach Update Admin ist
+ var willBeAdmin int
+ if req.IsAdmin != nil {
+ if *req.IsAdmin {
+ willBeAdmin = 1
+ }
+ } else {
+ willBeAdmin = isAdmin
+ }
+
+ // Nur Gruppen zuweisen wenn User nicht Admin ist
+ if willBeAdmin == 0 {
+ // Lösche alle bestehenden Gruppen-Zuweisungen
+ _, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID)
+ if err != nil {
+ log.Printf("Fehler beim Löschen der Gruppen-Zuweisungen: %v", err)
+ }
+
+ // Füge neue Gruppen-Zuweisungen hinzu
+ for _, groupID := range req.GroupIDs {
+ // Prüfe ob Gruppe existiert
+ var exists bool
+ err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM permission_groups WHERE id = ?)", groupID).Scan(&exists)
+ if err == nil && exists {
+ _, err = db.ExecContext(ctx, "INSERT INTO user_groups (user_id, group_id) VALUES (?, ?)", userID, groupID)
+ if err != nil {
+ log.Printf("Fehler beim Zuweisen der Gruppe %s zum Benutzer: %v", groupID, err)
+ }
+ }
+ }
+ }
+ }
+
// Lade aktualisierten Benutzer
var user User
- err = db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID).
- Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
+ var isAdminUpdated, enabledUpdated int
+ err = db.QueryRowContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users WHERE id = ?", userID).
+ Scan(&user.ID, &user.Username, &user.Email, &isAdminUpdated, &enabledUpdated, &user.CreatedAt)
if err != nil {
http.Error(w, "Fehler beim Abrufen des aktualisierten Benutzers", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen des aktualisierten Benutzers: %v", err)
return
}
+ user.IsAdmin = isAdminUpdated == 1
+ user.Enabled = enabledUpdated == 1
+
+ // Lade Gruppen-IDs (nur wenn nicht Admin)
+ if user.IsAdmin {
+ user.GroupIDs = []string{} // Admins haben keine Gruppen
+ } else if req.GroupIDs != nil {
+ user.GroupIDs = req.GroupIDs
+ } else {
+ groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID)
+ if err == nil {
+ var groupIDs []string
+ for groupRows.Next() {
+ var groupID string
+ if err := groupRows.Scan(&groupID); err == nil {
+ groupIDs = append(groupIDs, groupID)
+ }
+ }
+ groupRows.Close()
+ user.GroupIDs = groupIDs
+ } else {
+ user.GroupIDs = []string{}
+ }
+ }
json.NewEncoder(w).Encode(user)
// Audit-Log: User aktualisiert
- userID, username := getUserFromRequest(r)
+ requestUserID, requestUsername := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
details := map[string]interface{}{}
if req.Username != "" {
@@ -2684,7 +3519,26 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
if req.Password != "" {
details["passwordChanged"] = true
}
- auditService.Track(r.Context(), "UPDATE", "user", vars["id"], userID, username, details, ipAddress, userAgent)
+ if req.IsAdmin != nil {
+ details["isAdmin"] = *req.IsAdmin
+ if *req.IsAdmin {
+ details["message"] = "Benutzer wurde zum Administrator ernannt"
+ } else {
+ details["message"] = "Administrator-Rechte wurden entfernt"
+ }
+ }
+ if req.Enabled != nil {
+ details["enabled"] = *req.Enabled
+ if *req.Enabled {
+ details["message"] = "Benutzer wurde aktiviert"
+ } else {
+ details["message"] = "Benutzer wurde deaktiviert"
+ }
+ }
+ if req.GroupIDs != nil {
+ details["groupIds"] = req.GroupIDs
+ }
+ auditService.Track(r.Context(), "UPDATE", "user", vars["id"], requestUserID, requestUsername, details, ipAddress, userAgent)
}
func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
@@ -2701,9 +3555,32 @@ func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
userID := vars["id"]
- ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
+ // Prüfe ob der zu löschende User der spezielle Admin-User mit UID "admin" ist
+ // Nur dieser User kann nicht gelöscht werden, andere Admin-User können gelöscht werden
+ if userID == "admin" {
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(map[string]string{
+ "error": "Der Administrator-Benutzer mit UID 'admin' kann nicht gelöscht werden. Verwenden Sie stattdessen die Deaktivierungsfunktion.",
+ })
+ return
+ }
+
+ // Prüfe ob User existiert
+ var exists bool
+ err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE id = ?)", userID).Scan(&exists)
+ if err != nil {
+ http.Error(w, "Fehler beim Prüfen des Benutzers", http.StatusInternalServerError)
+ log.Printf("Fehler beim Prüfen des Benutzers: %v", err)
+ return
+ }
+ if !exists {
+ http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
+ return
+ }
+
result, err := db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", userID)
if err != nil {
http.Error(w, "Fehler beim Löschen des Benutzers", http.StatusInternalServerError)
@@ -2726,9 +3603,9 @@ func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(response)
// Audit-Log: User gelöscht
- userID, username := getUserFromRequest(r)
+ requestUserID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
- auditService.Track(r.Context(), "DELETE", "user", vars["id"], userID, username, map[string]interface{}{
+ auditService.Track(r.Context(), "DELETE", "user", vars["id"], requestUserID, username, map[string]interface{}{
"message": fmt.Sprintf("User gelöscht: %s", vars["id"]),
}, ipAddress, userAgent)
}
@@ -2921,10 +3798,400 @@ func getAvatarHandler(w http.ResponseWriter, r *http.Request) {
io.Copy(w, file)
}
+// Permission Groups Handler Functions
+
+func getPermissionGroupsHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
+
+ if r.Method == "OPTIONS" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+ defer cancel()
+
+ // Lade alle Gruppen
+ rows, err := db.QueryContext(ctx, "SELECT id, name, description, permission, created_at FROM permission_groups ORDER BY created_at DESC")
+ if err != nil {
+ http.Error(w, "Fehler beim Abrufen der Berechtigungsgruppen", http.StatusInternalServerError)
+ log.Printf("Fehler beim Abrufen der Berechtigungsgruppen: %v", err)
+ return
+ }
+ defer rows.Close()
+
+ var groups []PermissionGroup
+ var groupIDs []string
+ for rows.Next() {
+ var group PermissionGroup
+ err := rows.Scan(&group.ID, &group.Name, &group.Description, &group.Permission, &group.CreatedAt)
+ if err != nil {
+ http.Error(w, "Fehler beim Lesen der Gruppen-Daten", http.StatusInternalServerError)
+ log.Printf("Fehler beim Lesen der Gruppen-Daten: %v", err)
+ return
+ }
+ group.SpaceIDs = []string{} // Initialisiere als leeres Array
+ groups = append(groups, group)
+ groupIDs = append(groupIDs, group.ID)
+ }
+ rows.Close()
+
+ // Lade alle Space-Zuweisungen in einem einzigen Query
+ if len(groupIDs) > 0 {
+ // Erstelle Platzhalter für IN-Clause
+ placeholders := make([]string, len(groupIDs))
+ args := make([]interface{}, len(groupIDs))
+ for i, id := range groupIDs {
+ placeholders[i] = "?"
+ args[i] = id
+ }
+
+ query := fmt.Sprintf("SELECT group_id, space_id FROM group_spaces WHERE group_id IN (%s)", strings.Join(placeholders, ","))
+ spaceRows, err := db.QueryContext(ctx, query, args...)
+ if err == nil {
+ // Erstelle eine Map von group_id zu space_ids
+ spaceMap := make(map[string][]string)
+ for spaceRows.Next() {
+ var groupID, spaceID string
+ if err := spaceRows.Scan(&groupID, &spaceID); err == nil {
+ spaceMap[groupID] = append(spaceMap[groupID], spaceID)
+ }
+ }
+ spaceRows.Close()
+
+ // Weise Space-IDs den Gruppen zu
+ for i := range groups {
+ if spaceIDs, exists := spaceMap[groups[i].ID]; exists {
+ groups[i].SpaceIDs = spaceIDs
+ }
+ }
+ }
+ }
+
+ json.NewEncoder(w).Encode(groups)
+}
+
+func getPermissionGroupHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
+
+ if r.Method == "OPTIONS" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ vars := mux.Vars(r)
+ groupID := vars["id"]
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+ defer cancel()
+
+ var group PermissionGroup
+ err := db.QueryRowContext(ctx, "SELECT id, name, description, permission, created_at FROM permission_groups WHERE id = ?", groupID).
+ Scan(&group.ID, &group.Name, &group.Description, &group.Permission, &group.CreatedAt)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ http.Error(w, "Berechtigungsgruppe nicht gefunden", http.StatusNotFound)
+ return
+ }
+ http.Error(w, "Fehler beim Abrufen der Berechtigungsgruppe", http.StatusInternalServerError)
+ log.Printf("Fehler beim Abrufen der Berechtigungsgruppe: %v", err)
+ return
+ }
+
+ // Lade Space-IDs für diese Gruppe
+ spaceRows, err := db.QueryContext(ctx, "SELECT space_id FROM group_spaces WHERE group_id = ?", groupID)
+ if err == nil {
+ var spaceIDs []string
+ for spaceRows.Next() {
+ var spaceID string
+ if err := spaceRows.Scan(&spaceID); err == nil {
+ spaceIDs = append(spaceIDs, spaceID)
+ }
+ }
+ spaceRows.Close()
+ group.SpaceIDs = spaceIDs
+ }
+
+ json.NewEncoder(w).Encode(group)
+}
+
+func createPermissionGroupHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
+
+ if r.Method == "OPTIONS" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ var req CreatePermissionGroupRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "Ungültige Anfrage", http.StatusBadRequest)
+ return
+ }
+
+ // Validierung
+ if req.Name == "" {
+ http.Error(w, "Name ist erforderlich", http.StatusBadRequest)
+ return
+ }
+
+ // Validiere Berechtigungsstufe
+ if req.Permission != PermissionRead && req.Permission != PermissionReadWrite && req.Permission != PermissionFullAccess {
+ http.Error(w, "Ungültige Berechtigungsstufe. Erlaubt: READ, READ_WRITE, FULL_ACCESS", http.StatusBadRequest)
+ return
+ }
+
+ // Erstelle Gruppe
+ groupID := uuid.New().String()
+ createdAt := time.Now().Format(time.RFC3339)
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+ defer cancel()
+
+ _, err := db.ExecContext(ctx,
+ "INSERT INTO permission_groups (id, name, description, permission, created_at) VALUES (?, ?, ?, ?, ?)",
+ groupID, req.Name, req.Description, string(req.Permission), createdAt)
+ if err != nil {
+ if strings.Contains(err.Error(), "UNIQUE constraint failed") {
+ http.Error(w, "Gruppenname bereits vergeben", http.StatusConflict)
+ return
+ }
+ http.Error(w, "Fehler beim Erstellen der Berechtigungsgruppe", http.StatusInternalServerError)
+ log.Printf("Fehler beim Erstellen der Berechtigungsgruppe: %v", err)
+ return
+ }
+
+ // Weise Spaces zu, falls angegeben
+ if len(req.SpaceIDs) > 0 {
+ for _, spaceID := range req.SpaceIDs {
+ // Prüfe ob Space existiert
+ var exists bool
+ err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists)
+ if err == nil && exists {
+ _, err = db.ExecContext(ctx, "INSERT OR IGNORE INTO group_spaces (group_id, space_id) VALUES (?, ?)", groupID, spaceID)
+ if err != nil {
+ log.Printf("Fehler beim Zuweisen des Space %s zur Gruppe: %v", spaceID, err)
+ }
+ }
+ }
+ }
+
+ group := PermissionGroup{
+ ID: groupID,
+ Name: req.Name,
+ Description: req.Description,
+ Permission: req.Permission,
+ SpaceIDs: req.SpaceIDs,
+ CreatedAt: createdAt,
+ }
+
+ w.WriteHeader(http.StatusCreated)
+ json.NewEncoder(w).Encode(group)
+
+ // Audit-Log
+ requestUserID, requestUsername := getUserFromRequest(r)
+ ipAddress, userAgent := getRequestInfo(r)
+ auditService.Track(r.Context(), "CREATE", "permission_group", groupID, requestUserID, requestUsername, map[string]interface{}{
+ "name": req.Name,
+ "permission": req.Permission,
+ "spaceIds": req.SpaceIDs,
+ "message": fmt.Sprintf("Berechtigungsgruppe erstellt: %s", req.Name),
+ }, ipAddress, userAgent)
+}
+
+func updatePermissionGroupHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "PUT, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
+
+ if r.Method == "OPTIONS" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ vars := mux.Vars(r)
+ groupID := vars["id"]
+
+ var req UpdatePermissionGroupRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "Ungültige Anfrage", http.StatusBadRequest)
+ return
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+ defer cancel()
+
+ // Prüfe ob Gruppe existiert
+ var exists bool
+ err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM permission_groups WHERE id = ?)", groupID).Scan(&exists)
+ if err != nil || !exists {
+ http.Error(w, "Berechtigungsgruppe nicht gefunden", http.StatusNotFound)
+ return
+ }
+
+ // Validiere Berechtigungsstufe, falls angegeben
+ if req.Permission != "" {
+ if req.Permission != PermissionRead && req.Permission != PermissionReadWrite && req.Permission != PermissionFullAccess {
+ http.Error(w, "Ungültige Berechtigungsstufe. Erlaubt: READ, READ_WRITE, FULL_ACCESS", http.StatusBadRequest)
+ return
+ }
+ }
+
+ // Update Felder
+ updates := []string{}
+ args := []interface{}{}
+
+ if req.Name != "" {
+ updates = append(updates, "name = ?")
+ args = append(args, req.Name)
+ }
+ if req.Description != "" {
+ updates = append(updates, "description = ?")
+ args = append(args, req.Description)
+ }
+ if req.Permission != "" {
+ updates = append(updates, "permission = ?")
+ args = append(args, string(req.Permission))
+ }
+
+ if len(updates) > 0 {
+ args = append(args, groupID)
+ query := fmt.Sprintf("UPDATE permission_groups SET %s WHERE id = ?", strings.Join(updates, ", "))
+ _, err = db.ExecContext(ctx, query, args...)
+ if err != nil {
+ if strings.Contains(err.Error(), "UNIQUE constraint failed") {
+ http.Error(w, "Gruppenname bereits vergeben", http.StatusConflict)
+ return
+ }
+ http.Error(w, "Fehler beim Aktualisieren der Berechtigungsgruppe", http.StatusInternalServerError)
+ log.Printf("Fehler beim Aktualisieren der Berechtigungsgruppe: %v", err)
+ return
+ }
+ }
+
+ // Aktualisiere Space-Zuweisungen, falls angegeben
+ if req.SpaceIDs != nil {
+ // Lösche alle bestehenden Space-Zuweisungen
+ _, err = db.ExecContext(ctx, "DELETE FROM group_spaces WHERE group_id = ?", groupID)
+ if err != nil {
+ log.Printf("Fehler beim Löschen der Space-Zuweisungen: %v", err)
+ }
+
+ // Füge neue Space-Zuweisungen hinzu
+ for _, spaceID := range req.SpaceIDs {
+ // Prüfe ob Space existiert
+ var exists bool
+ err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists)
+ if err == nil && exists {
+ _, err = db.ExecContext(ctx, "INSERT INTO group_spaces (group_id, space_id) VALUES (?, ?)", groupID, spaceID)
+ if err != nil {
+ log.Printf("Fehler beim Zuweisen des Space %s zur Gruppe: %v", spaceID, err)
+ }
+ }
+ }
+ }
+
+ // Lade aktualisierte Gruppe
+ var group PermissionGroup
+ err = db.QueryRowContext(ctx, "SELECT id, name, description, permission, created_at FROM permission_groups WHERE id = ?", groupID).
+ Scan(&group.ID, &group.Name, &group.Description, &group.Permission, &group.CreatedAt)
+ if err != nil {
+ http.Error(w, "Fehler beim Abrufen der aktualisierten Gruppe", http.StatusInternalServerError)
+ log.Printf("Fehler beim Abrufen der aktualisierten Gruppe: %v", err)
+ return
+ }
+
+ // Lade Space-IDs
+ if req.SpaceIDs != nil {
+ group.SpaceIDs = req.SpaceIDs
+ } else {
+ spaceRows, err := db.QueryContext(ctx, "SELECT space_id FROM group_spaces WHERE group_id = ?", groupID)
+ if err == nil {
+ var spaceIDs []string
+ for spaceRows.Next() {
+ var spaceID string
+ if err := spaceRows.Scan(&spaceID); err == nil {
+ spaceIDs = append(spaceIDs, spaceID)
+ }
+ }
+ spaceRows.Close()
+ group.SpaceIDs = spaceIDs
+ }
+ }
+
+ json.NewEncoder(w).Encode(group)
+
+ // Audit-Log
+ requestUserID, requestUsername := getUserFromRequest(r)
+ ipAddress, userAgent := getRequestInfo(r)
+ auditService.Track(r.Context(), "UPDATE", "permission_group", groupID, requestUserID, requestUsername, map[string]interface{}{
+ "name": req.Name,
+ "permission": req.Permission,
+ "spaceIds": req.SpaceIDs,
+ "message": fmt.Sprintf("Berechtigungsgruppe aktualisiert: %s", groupID),
+ }, ipAddress, userAgent)
+}
+
+func deletePermissionGroupHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
+
+ if r.Method == "OPTIONS" {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ vars := mux.Vars(r)
+ groupID := vars["id"]
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+ defer cancel()
+
+ result, err := db.ExecContext(ctx, "DELETE FROM permission_groups WHERE id = ?", groupID)
+ if err != nil {
+ http.Error(w, "Fehler beim Löschen der Berechtigungsgruppe", http.StatusInternalServerError)
+ log.Printf("Fehler beim Löschen der Berechtigungsgruppe: %v", err)
+ return
+ }
+
+ rowsAffected, err := result.RowsAffected()
+ if err != nil {
+ http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError)
+ return
+ }
+
+ if rowsAffected == 0 {
+ http.Error(w, "Berechtigungsgruppe nicht gefunden", http.StatusNotFound)
+ return
+ }
+
+ response := MessageResponse{Message: "Berechtigungsgruppe erfolgreich gelöscht"}
+ json.NewEncoder(w).Encode(response)
+
+ // Audit-Log
+ requestUserID, requestUsername := getUserFromRequest(r)
+ ipAddress, userAgent := getRequestInfo(r)
+ auditService.Track(r.Context(), "DELETE", "permission_group", groupID, requestUserID, requestUsername, map[string]interface{}{
+ "message": fmt.Sprintf("Berechtigungsgruppe gelöscht: %s", groupID),
+ }, ipAddress, userAgent)
+}
+
// Passwortvalidierung nach Richtlinien
func validatePassword(password string) error {
if len(password) < 8 {
- return fmt.Errorf("Passwort muss mindestens 8 Zeichen lang sein")
+ return fmt.Errorf("passwort muss mindestens 8 Zeichen lang sein")
}
hasUpper := false
@@ -2963,7 +4230,7 @@ func validatePassword(password string) error {
}
if len(missing) > 0 {
- return fmt.Errorf("Passwort muss enthalten: %s", strings.Join(missing, ", "))
+ return fmt.Errorf("passwort muss enthalten: %s", strings.Join(missing, ", "))
}
return nil
@@ -2990,18 +4257,357 @@ func getUserFromRequest(r *http.Request) (userID, username string) {
username = parts[0]
// Hole User-ID aus der Datenbank
- ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
var id string
- err = db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = ?", username).Scan(&id)
+ var enabled int
+ err = db.QueryRowContext(ctx, "SELECT id, enabled FROM users WHERE username = ?", username).Scan(&id, &enabled)
if err != nil {
+ // Logge Fehler nur wenn es nicht "no rows" ist
+ if err != sql.ErrNoRows {
+ log.Printf("Fehler beim Abrufen der User-ID für %s: %v", username, err)
+ }
+ return "", username
+ }
+
+ // Prüfe ob User aktiviert ist
+ if enabled == 0 {
+ log.Printf("API-Zugriff für deaktivierten Benutzer: %s", username)
return "", username
}
return id, username
}
+// UserPermissionInfo enthält die Berechtigungsinformationen eines Benutzers
+type UserPermissionInfo struct {
+ UserID string
+ Groups []PermissionGroupInfo
+ HasFullAccess bool // true wenn der Benutzer mindestens eine FULL_ACCESS Gruppe hat
+}
+
+// PermissionGroupInfo enthält Informationen über eine Berechtigungsgruppe
+type PermissionGroupInfo struct {
+ GroupID string
+ Permission PermissionLevel
+ SpaceIDs []string // Leer bedeutet Zugriff auf alle Spaces
+}
+
+// isUserAdmin prüft, ob ein Benutzer Admin ist
+func isUserAdmin(userID string) (bool, error) {
+ if userID == "" {
+ return false, nil
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+ defer cancel()
+
+ var isAdmin int
+ err := db.QueryRowContext(ctx, "SELECT is_admin FROM users WHERE id = ?", userID).Scan(&isAdmin)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return false, nil
+ }
+ return false, err
+ }
+
+ return isAdmin == 1, nil
+}
+
+// getUserPermissions ruft die Berechtigungen eines Benutzers ab
+func getUserPermissions(userID string) (*UserPermissionInfo, error) {
+ if userID == "" {
+ return nil, fmt.Errorf("userID ist leer")
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+ defer cancel()
+
+ // Prüfe ob User Admin ist - Admins haben immer Vollzugriff
+ isAdmin, err := isUserAdmin(userID)
+ if err != nil {
+ log.Printf("Fehler beim Prüfen des Admin-Status: %v", err)
+ }
+ if isAdmin {
+ return &UserPermissionInfo{
+ UserID: userID,
+ Groups: []PermissionGroupInfo{},
+ HasFullAccess: true,
+ }, nil
+ }
+
+ // Hole alle Gruppen des Benutzers mit ihren Berechtigungen
+ query := `
+ SELECT pg.id, pg.permission
+ FROM permission_groups pg
+ INNER JOIN user_groups ug ON pg.id = ug.group_id
+ WHERE ug.user_id = ?
+ `
+ rows, err := db.QueryContext(ctx, query, userID)
+ if err != nil {
+ return nil, fmt.Errorf("fehler beim Abrufen der Benutzergruppen: %w", err)
+ }
+ defer rows.Close()
+
+ info := &UserPermissionInfo{
+ UserID: userID,
+ Groups: []PermissionGroupInfo{},
+ }
+
+ var groupIDs []string
+ for rows.Next() {
+ var groupID string
+ var permission string
+ if err := rows.Scan(&groupID, &permission); err != nil {
+ continue
+ }
+ groupIDs = append(groupIDs, groupID)
+
+ groupInfo := PermissionGroupInfo{
+ GroupID: groupID,
+ Permission: PermissionLevel(permission),
+ SpaceIDs: []string{},
+ }
+
+ if PermissionLevel(permission) == PermissionFullAccess {
+ info.HasFullAccess = true
+ }
+
+ info.Groups = append(info.Groups, groupInfo)
+ }
+
+ // Hole Space-Zuweisungen für alle Gruppen
+ if len(groupIDs) > 0 {
+ placeholders := make([]string, len(groupIDs))
+ args := make([]interface{}, len(groupIDs))
+ for i, id := range groupIDs {
+ placeholders[i] = "?"
+ args[i] = id
+ }
+
+ spaceQuery := fmt.Sprintf(`
+ SELECT group_id, space_id
+ FROM group_spaces
+ WHERE group_id IN (%s)
+ `, strings.Join(placeholders, ","))
+
+ spaceRows, err := db.QueryContext(ctx, spaceQuery, args...)
+ if err == nil {
+ spaceMap := make(map[string][]string)
+ for spaceRows.Next() {
+ var groupID, spaceID string
+ if err := spaceRows.Scan(&groupID, &spaceID); err == nil {
+ spaceMap[groupID] = append(spaceMap[groupID], spaceID)
+ }
+ }
+ spaceRows.Close()
+
+ // Aktualisiere SpaceIDs für jede Gruppe
+ for i := range info.Groups {
+ if spaceIDs, exists := spaceMap[info.Groups[i].GroupID]; exists {
+ info.Groups[i].SpaceIDs = spaceIDs
+ }
+ }
+ }
+ }
+
+ return info, nil
+}
+
+// hasSpaceAccess prüft, ob ein Benutzer Zugriff auf einen bestimmten Space hat
+func hasSpaceAccess(userID, spaceID string) (bool, error) {
+ if userID == "" {
+ return false, nil
+ }
+
+ // Admins haben immer Zugriff
+ isAdmin, err := isUserAdmin(userID)
+ if err == nil && isAdmin {
+ return true, nil
+ }
+
+ permissions, err := getUserPermissions(userID)
+ if err != nil {
+ return false, err
+ }
+
+ // Wenn der Benutzer keine Gruppen hat und nicht Admin ist, hat er keinen Zugriff
+ // Admins haben immer Zugriff (wird bereits oben geprüft)
+ if !isAdmin && len(permissions.Groups) == 0 {
+ return false, nil
+ }
+
+ // Prüfe für jede Gruppe, ob der Benutzer Zugriff auf den Space hat
+ for _, group := range permissions.Groups {
+ // Wenn die Gruppe keine Spaces zugewiesen hat, hat der Benutzer Zugriff auf alle Spaces
+ if len(group.SpaceIDs) == 0 {
+ return true, nil
+ }
+ // Prüfe, ob der Space in der Liste der zugewiesenen Spaces ist
+ for _, assignedSpaceID := range group.SpaceIDs {
+ if assignedSpaceID == spaceID {
+ return true, nil
+ }
+ }
+ }
+
+ return false, nil
+}
+
+// hasPermission prüft, ob ein Benutzer eine bestimmte Berechtigung für einen Space hat
+// requiredPermission kann READ, READ_WRITE oder FULL_ACCESS sein
+func hasPermission(userID, spaceID string, requiredPermission PermissionLevel) (bool, error) {
+ if userID == "" {
+ return false, nil
+ }
+
+ // Admins haben immer alle Berechtigungen
+ isAdmin, err := isUserAdmin(userID)
+ if err == nil && isAdmin {
+ return true, nil
+ }
+
+ permissions, err := getUserPermissions(userID)
+ if err != nil {
+ return false, err
+ }
+
+ // Wenn der Benutzer keine Gruppen hat und nicht Admin ist, hat er keine Berechtigung
+ // Admins haben immer alle Berechtigungen (wird bereits oben geprüft)
+ if !isAdmin && len(permissions.Groups) == 0 {
+ return false, nil
+ }
+
+ // Prüfe für jede Gruppe
+ for _, group := range permissions.Groups {
+ hasAccess := false
+
+ // Prüfe, ob der Benutzer Zugriff auf den Space hat
+ if len(group.SpaceIDs) == 0 {
+ // Keine Space-Zuweisungen = Zugriff auf alle Spaces
+ hasAccess = true
+ } else {
+ // Prüfe, ob der Space in der Liste ist
+ for _, assignedSpaceID := range group.SpaceIDs {
+ if assignedSpaceID == spaceID {
+ hasAccess = true
+ break
+ }
+ }
+ }
+
+ if !hasAccess {
+ continue
+ }
+
+ // Prüfe die Berechtigungsstufe
+ switch requiredPermission {
+ case PermissionRead:
+ // READ, READ_WRITE und FULL_ACCESS haben alle READ-Berechtigung
+ return true, nil
+ case PermissionReadWrite:
+ // READ_WRITE und FULL_ACCESS haben READ_WRITE-Berechtigung
+ if group.Permission == PermissionReadWrite || group.Permission == PermissionFullAccess {
+ return true, nil
+ }
+ case PermissionFullAccess:
+ // Nur FULL_ACCESS hat FULL_ACCESS-Berechtigung
+ if group.Permission == PermissionFullAccess {
+ return true, nil
+ }
+ }
+ }
+
+ return false, nil
+}
+
+// getAccessibleSpaceIDs gibt alle Space-IDs zurück, auf die der Benutzer Zugriff hat
+func getAccessibleSpaceIDs(userID string) ([]string, error) {
+ if userID == "" {
+ return []string{}, nil
+ }
+
+ // Prüfe ob User Admin ist - Admins haben Zugriff auf alle Spaces
+ isAdmin, err := isUserAdmin(userID)
+ if err == nil && isAdmin {
+ // Hole alle Spaces für Admin
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+ defer cancel()
+
+ rows, err := db.QueryContext(ctx, "SELECT id FROM spaces")
+ if err != nil {
+ return []string{}, err
+ }
+ defer rows.Close()
+
+ var spaceIDs []string
+ for rows.Next() {
+ var spaceID string
+ if err := rows.Scan(&spaceID); err == nil {
+ spaceIDs = append(spaceIDs, spaceID)
+ }
+ }
+ return spaceIDs, nil
+ }
+
+ permissions, err := getUserPermissions(userID)
+ if err != nil {
+ return []string{}, err
+ }
+
+ // Wenn der Benutzer keine Gruppen hat, hat er keinen Zugriff
+ // (Admin wurde bereits oben behandelt)
+ if len(permissions.Groups) == 0 {
+ return []string{}, nil
+ }
+
+ // Sammle alle zugewiesenen Spaces
+ spaceIDMap := make(map[string]bool)
+ hasUnrestrictedAccess := false
+
+ for _, group := range permissions.Groups {
+ // Wenn eine Gruppe keine Spaces zugewiesen hat, hat der Benutzer Zugriff auf alle Spaces
+ if len(group.SpaceIDs) == 0 {
+ hasUnrestrictedAccess = true
+ break
+ }
+ // Sammle alle zugewiesenen Spaces
+ for _, spaceID := range group.SpaceIDs {
+ spaceIDMap[spaceID] = true
+ }
+ }
+
+ // Wenn der Benutzer Zugriff auf alle Spaces hat, hole alle Spaces aus der DB
+ if hasUnrestrictedAccess {
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
+ defer cancel()
+
+ rows, err := db.QueryContext(ctx, "SELECT id FROM spaces")
+ if err != nil {
+ return []string{}, err
+ }
+ defer rows.Close()
+
+ var spaceIDs []string
+ for rows.Next() {
+ var spaceID string
+ if err := rows.Scan(&spaceID); err == nil {
+ spaceIDs = append(spaceIDs, spaceID)
+ }
+ }
+ return spaceIDs, nil
+ }
+
+ // Konvertiere Map zu Slice
+ spaceIDs := make([]string, 0, len(spaceIDMap))
+ for spaceID := range spaceIDMap {
+ spaceIDs = append(spaceIDs, spaceID)
+ }
+
+ return spaceIDs, nil
+}
+
// Helper-Funktion zum Extrahieren von IP-Adresse und User-Agent aus Request
func getRequestInfo(r *http.Request) (ipAddress, userAgent string) {
// Hole IP-Adresse
@@ -3014,13 +4620,13 @@ func getRequestInfo(r *http.Request) (ipAddress, userAgent string) {
// Hole User-Agent
userAgent = r.Header.Get("User-Agent")
-
+
// Prüfe, ob es sich um einen API-Aufruf handelt
// API-Aufrufe haben entweder keinen User-Agent oder einen speziellen Header
if userAgent == "" || r.Header.Get("X-API-Request") == "true" || r.Header.Get("X-Request-Source") == "api" {
userAgent = "API"
}
-
+
return ipAddress, userAgent
}
@@ -3099,7 +4705,7 @@ func getAuditLogsHandler(w http.ResponseWriter, r *http.Request) {
// Hole Logs - Verwende Prepared Statement für bessere Kompatibilität
var querySQL string
var queryArgs []interface{}
-
+
if whereSQL != "" {
querySQL = fmt.Sprintf(`
SELECT id, timestamp, user_id, username, action, resource_type, resource_id, details, ip_address, user_agent
@@ -3123,7 +4729,7 @@ func getAuditLogsHandler(w http.ResponseWriter, r *http.Request) {
queryCtx, queryCancel := context.WithTimeout(context.Background(), time.Second*10)
defer queryCancel() // Cancel wird erst aufgerufen, wenn die Funktion beendet ist
-
+
rows, err := db.QueryContext(queryCtx, querySQL, queryArgs...)
if err != nil {
http.Error(w, "Fehler beim Abrufen der Logs", http.StatusInternalServerError)
@@ -3201,11 +4807,11 @@ func getAuditLogsHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Zeilen gelesen: %d, Scan-Fehler: %d, Logs hinzugefügt: %d", rowCount, scanErrors, len(logs))
response := map[string]interface{}{
- "logs": logs,
- "total": totalCount,
- "limit": limit,
- "offset": offset,
- "hasMore": offset+limit < totalCount,
+ "logs": logs,
+ "total": totalCount,
+ "limit": limit,
+ "offset": offset,
+ "hasMore": offset+limit < totalCount,
}
log.Printf("Audit-Logs abgerufen: %d Einträge gefunden (Total: %d, Limit: %d, Offset: %d)", len(logs), totalCount, limit, offset)
@@ -3275,14 +4881,14 @@ func createTestAuditLogHandler(w http.ResponseWriter, r *http.Request) {
}
var req struct {
- Action string `json:"action"`
- Entity string `json:"entity"`
- EntityID string `json:"entityID"`
- UserID string `json:"userID"`
- Username string `json:"username"`
- Details map[string]interface{} `json:"details"`
- IPAddress string `json:"ipAddress"`
- UserAgent string `json:"userAgent"`
+ Action string `json:"action"`
+ Entity string `json:"entity"`
+ EntityID string `json:"entityID"`
+ UserID string `json:"userID"`
+ Username string `json:"username"`
+ Details map[string]interface{} `json:"details"`
+ IPAddress string `json:"ipAddress"`
+ UserAgent string `json:"userAgent"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -3303,7 +4909,7 @@ func createTestAuditLogHandler(w http.ResponseWriter, r *http.Request) {
// Erstelle Context für die Anfrage
ctx := r.Context()
-
+
// Track das Event
auditService.Track(ctx, req.Action, req.Entity, req.EntityID, req.UserID, req.Username, req.Details, req.IPAddress, req.UserAgent)
@@ -3383,11 +4989,12 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
password := parts[1]
// Validiere Benutzer
- ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
var storedHash string
- err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE username = ?", username).Scan(&storedHash)
+ var enabled int
+ err = db.QueryRowContext(ctx, "SELECT password_hash, enabled FROM users WHERE username = ?", username).Scan(&storedHash, &enabled)
if err != nil {
if err == sql.ErrNoRows {
if !isAjaxRequest {
@@ -3405,6 +5012,17 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return
}
+ // Prüfe ob User aktiviert ist
+ if enabled == 0 {
+ if !isAjaxRequest {
+ w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`)
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Benutzerkonto ist deaktiviert"})
+ return
+ }
+
// Prüfe Passwort
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
if err != nil {
@@ -3422,6 +5040,37 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
}
}
+// adminOnlyMiddleware prüft, ob der Benutzer ein Admin ist
+func adminOnlyMiddleware(next http.HandlerFunc) http.HandlerFunc {
+ return basicAuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
+ userID, _ := getUserFromRequest(r)
+ if userID == "" {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusUnauthorized)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Nicht authentifiziert"})
+ return
+ }
+
+ isAdmin, err := isUserAdmin(userID)
+ if err != nil {
+ log.Printf("Fehler beim Prüfen des Admin-Status: %v", err)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Fehler beim Prüfen der Berechtigung"})
+ return
+ }
+
+ if !isAdmin {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Nur Administratoren haben Zugriff auf diese Funktion"})
+ return
+ }
+
+ next(w, r)
+ })
+}
+
// Login Handler für Frontend (validiert Basic Auth und gibt User-Info zurück)
func loginHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
@@ -3474,13 +5123,14 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Login-Versuch für Benutzer: %s", username)
// Validiere Benutzer
- ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
var user User
var storedHash string
- err = db.QueryRowContext(ctx, "SELECT id, username, email, password_hash, created_at FROM users WHERE username = ?", username).
- Scan(&user.ID, &user.Username, &user.Email, &storedHash, &user.CreatedAt)
+ var enabled int
+ err = db.QueryRowContext(ctx, "SELECT id, username, email, password_hash, enabled, created_at FROM users WHERE username = ?", username).
+ Scan(&user.ID, &user.Username, &user.Email, &storedHash, &enabled, &user.CreatedAt)
if err != nil {
if err == sql.ErrNoRows {
log.Printf("Benutzer nicht gefunden: %s", username)
@@ -3494,6 +5144,14 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prüfe ob User aktiviert ist
+ if enabled == 0 {
+ log.Printf("Login-Versuch für deaktivierten Benutzer: %s", username)
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Benutzerkonto ist deaktiviert"})
+ return
+ }
+
// Prüfe Passwort
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
if err != nil {
@@ -3518,7 +5176,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
func main() {
log.Println("Starte certigo-addon Backend...")
-
+
// Initialisiere Datenbank
log.Println("Initialisiere Datenbank...")
initDB()
@@ -3542,34 +5200,42 @@ func main() {
// API Routes
api := r.PathPrefix("/api").Subrouter()
-
+
// Public Routes (keine Auth erforderlich)
api.HandleFunc("/health", healthHandler).Methods("GET", "OPTIONS")
api.HandleFunc("/login", loginHandler).Methods("POST", "OPTIONS")
-
+
// Protected Routes (Basic Auth erforderlich)
api.HandleFunc("/stats", basicAuthMiddleware(getStatsHandler)).Methods("GET", "OPTIONS")
- api.HandleFunc("/spaces", getSpacesHandler).Methods("GET", "OPTIONS")
- api.HandleFunc("/spaces", createSpaceHandler).Methods("POST", "OPTIONS")
- api.HandleFunc("/spaces/{id}", deleteSpaceHandler).Methods("DELETE", "OPTIONS")
- api.HandleFunc("/spaces/{id}/fqdns/count", getSpaceFqdnCountHandler).Methods("GET", "OPTIONS")
- api.HandleFunc("/spaces/{id}/fqdns", getFqdnsHandler).Methods("GET", "OPTIONS")
- api.HandleFunc("/spaces/{id}/fqdns", createFqdnHandler).Methods("POST", "OPTIONS")
- api.HandleFunc("/spaces/{id}/fqdns", deleteAllFqdnsHandler).Methods("DELETE", "OPTIONS")
- api.HandleFunc("/spaces/{id}/fqdns/{fqdnId}", deleteFqdnHandler).Methods("DELETE", "OPTIONS")
- api.HandleFunc("/fqdns", deleteAllFqdnsGlobalHandler).Methods("DELETE", "OPTIONS")
- api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", uploadCSRHandler).Methods("POST", "OPTIONS")
- api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", getCSRByFQDNHandler).Methods("GET", "OPTIONS")
- api.HandleFunc("/csrs", deleteAllCSRsHandler).Methods("DELETE", "OPTIONS")
+ api.HandleFunc("/spaces", basicAuthMiddleware(getSpacesHandler)).Methods("GET", "OPTIONS")
+ api.HandleFunc("/spaces", basicAuthMiddleware(createSpaceHandler)).Methods("POST", "OPTIONS")
+ api.HandleFunc("/spaces/{id}", basicAuthMiddleware(deleteSpaceHandler)).Methods("DELETE", "OPTIONS")
+ api.HandleFunc("/spaces/{id}/fqdns/count", basicAuthMiddleware(getSpaceFqdnCountHandler)).Methods("GET", "OPTIONS")
+ api.HandleFunc("/spaces/{id}/fqdns", basicAuthMiddleware(getFqdnsHandler)).Methods("GET", "OPTIONS")
+ api.HandleFunc("/spaces/{id}/fqdns", basicAuthMiddleware(createFqdnHandler)).Methods("POST", "OPTIONS")
+ api.HandleFunc("/spaces/{id}/fqdns", basicAuthMiddleware(deleteAllFqdnsHandler)).Methods("DELETE", "OPTIONS")
+ api.HandleFunc("/spaces/{id}/fqdns/{fqdnId}", basicAuthMiddleware(deleteFqdnHandler)).Methods("DELETE", "OPTIONS")
+ api.HandleFunc("/fqdns", basicAuthMiddleware(deleteAllFqdnsGlobalHandler)).Methods("DELETE", "OPTIONS")
+ api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", basicAuthMiddleware(uploadCSRHandler)).Methods("POST", "OPTIONS")
+ api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", basicAuthMiddleware(getCSRByFQDNHandler)).Methods("GET", "OPTIONS")
+ api.HandleFunc("/csrs", basicAuthMiddleware(deleteAllCSRsHandler)).Methods("DELETE", "OPTIONS")
- // User Routes
- api.HandleFunc("/users", getUsersHandler).Methods("GET", "OPTIONS")
- api.HandleFunc("/users", createUserHandler).Methods("POST", "OPTIONS")
- api.HandleFunc("/users/{id}", getUserHandler).Methods("GET", "OPTIONS")
- api.HandleFunc("/users/{id}", updateUserHandler).Methods("PUT", "OPTIONS")
- api.HandleFunc("/users/{id}", deleteUserHandler).Methods("DELETE", "OPTIONS")
+ // User Routes (Admin only)
+ api.HandleFunc("/users", adminOnlyMiddleware(getUsersHandler)).Methods("GET", "OPTIONS")
+ api.HandleFunc("/users", adminOnlyMiddleware(createUserHandler)).Methods("POST", "OPTIONS")
+ api.HandleFunc("/users/{id}", adminOnlyMiddleware(getUserHandler)).Methods("GET", "OPTIONS")
+ api.HandleFunc("/users/{id}", adminOnlyMiddleware(updateUserHandler)).Methods("PUT", "OPTIONS")
+ api.HandleFunc("/users/{id}", adminOnlyMiddleware(deleteUserHandler)).Methods("DELETE", "OPTIONS")
api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(getAvatarHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(uploadAvatarHandler)).Methods("POST", "OPTIONS")
+ api.HandleFunc("/user/permissions", basicAuthMiddleware(getUserPermissionsHandler)).Methods("GET", "OPTIONS")
+
+ // Permission Groups Routes (Admin only)
+ api.HandleFunc("/permission-groups", adminOnlyMiddleware(getPermissionGroupsHandler)).Methods("GET", "OPTIONS")
+ api.HandleFunc("/permission-groups", adminOnlyMiddleware(createPermissionGroupHandler)).Methods("POST", "OPTIONS")
+ api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(getPermissionGroupHandler)).Methods("GET", "OPTIONS")
+ api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(updatePermissionGroupHandler)).Methods("PUT", "OPTIONS")
+ api.HandleFunc("/permission-groups/{id}", adminOnlyMiddleware(deletePermissionGroupHandler)).Methods("DELETE", "OPTIONS")
// Provider Routes (Protected)
api.HandleFunc("/providers", basicAuthMiddleware(getProvidersHandler)).Methods("GET", "OPTIONS")
@@ -3860,6 +5526,25 @@ func signCSRHandler(w http.ResponseWriter, r *http.Request) {
spaceID := vars["spaceId"]
fqdnID := vars["fqdnId"]
+ // Prüfe Berechtigung: READ_WRITE oder FULL_ACCESS erforderlich zum Signieren von CSRs
+ userID, username := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ hasPermission, err := hasPermission(userID, spaceID, PermissionReadWrite)
+ if err != nil {
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ log.Printf("Fehler beim Prüfen der Berechtigung: %v", err)
+ return
+ }
+
+ if !hasPermission {
+ http.Error(w, "Keine Berechtigung zum Signieren von CSRs. Lesen/Schreiben oder Vollzugriff erforderlich.", http.StatusForbidden)
+ return
+ }
+
var req struct {
ProviderID string `json:"providerId"`
CSRID string `json:"csrId,omitempty"` // Optional: spezifischer CSR, sonst neuester
@@ -3878,7 +5563,7 @@ func signCSRHandler(w http.ResponseWriter, r *http.Request) {
// Hole neuesten CSR für den FQDN
var csrPEM string
var csrID string
- err := db.QueryRow(`
+ err = db.QueryRow(`
SELECT id, csr_pem
FROM csrs
WHERE fqdn_id = ? AND space_id = ?
@@ -3962,15 +5647,14 @@ func signCSRHandler(w http.ResponseWriter, r *http.Request) {
})
// Audit-Log: CSR signiert
- userID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
auditService.Track(r.Context(), "SIGN", "csr", csrID, userID, username, map[string]interface{}{
- "providerId": req.ProviderID,
- "fqdnId": fqdnID,
- "spaceId": spaceID,
+ "providerId": req.ProviderID,
+ "fqdnId": fqdnID,
+ "spaceId": spaceID,
"certificateId": result.OrderID,
- "status": result.Status,
- "message": fmt.Sprintf("CSR signiert mit Provider %s für FQDN %s (Certificate ID: %s)", req.ProviderID, fqdnID, result.OrderID),
+ "status": result.Status,
+ "message": fmt.Sprintf("CSR signiert mit Provider %s für FQDN %s (Certificate ID: %s)", req.ProviderID, fqdnID, result.OrderID),
}, ipAddress, userAgent)
}
@@ -3989,6 +5673,25 @@ func getCertificatesHandler(w http.ResponseWriter, r *http.Request) {
spaceID := vars["spaceId"]
fqdnID := vars["fqdnId"]
+ // Prüfe Berechtigung: Benutzer muss Zugriff auf den Space haben (READ, READ_WRITE oder FULL_ACCESS)
+ userID, _ := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ hasAccess, err := hasSpaceAccess(userID, spaceID)
+ if err != nil {
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ log.Printf("Fehler beim Prüfen der Berechtigung: %v", err)
+ return
+ }
+
+ if !hasAccess {
+ http.Error(w, "Keine Berechtigung für diesen Space", http.StatusForbidden)
+ return
+ }
+
// Hole alle Zertifikate für diesen FQDN
rows, err := db.Query(`
SELECT id, csr_id, certificate_id, provider_id, certificate_pem, status, created_at
@@ -4044,9 +5747,28 @@ func refreshCertificateHandler(w http.ResponseWriter, r *http.Request) {
fqdnID := vars["fqdnId"]
certID := vars["certId"]
+ // Prüfe Berechtigung: Benutzer muss Zugriff auf den Space haben (READ, READ_WRITE oder FULL_ACCESS)
+ userID, _ := getUserFromRequest(r)
+ if userID == "" {
+ http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized)
+ return
+ }
+
+ hasAccess, err := hasSpaceAccess(userID, spaceID)
+ if err != nil {
+ http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError)
+ log.Printf("Fehler beim Prüfen der Berechtigung: %v", err)
+ return
+ }
+
+ if !hasAccess {
+ http.Error(w, "Keine Berechtigung für diesen Space", http.StatusForbidden)
+ return
+ }
+
// Hole Zertifikat aus DB
var certificateID, providerID string
- err := db.QueryRow(`
+ err = db.QueryRow(`
SELECT certificate_id, provider_id
FROM certificates
WHERE id = ? AND fqdn_id = ? AND space_id = ?
diff --git a/backend/spaces.db-shm b/backend/spaces.db-shm
index d9f70fb..83ea5fb 100644
Binary files a/backend/spaces.db-shm and b/backend/spaces.db-shm differ
diff --git a/backend/spaces.db-wal b/backend/spaces.db-wal
index 647eeae..31d02e7 100644
Binary files a/backend/spaces.db-wal and b/backend/spaces.db-wal differ
diff --git a/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png b/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png
index 803e549..d601cd8 100644
Binary files a/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png and b/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png differ
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 5764df8..d030f7a 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider, useAuth } from './contexts/AuthContext'
+import { PermissionsProvider, usePermissions } from './contexts/PermissionsContext'
import Sidebar from './components/Sidebar'
import Footer from './components/Footer'
import Home from './pages/Home'
@@ -9,6 +10,8 @@ import SpaceDetail from './pages/SpaceDetail'
import Impressum from './pages/Impressum'
import Profile from './pages/Profile'
import Users from './pages/Users'
+import Permissions from './pages/Permissions'
+import Providers from './pages/Providers'
import Login from './pages/Login'
import AuditLogs from './pages/AuditLogs'
@@ -33,6 +36,85 @@ const ProtectedRoute = ({ children }) => {
return isAuthenticated ? children :
Lade...
+Zugriff verweigert
+Nur Administratoren haben Zugriff auf diese Seite.
+Lade...
++ {messageType === 'warning' ? 'Keine Berechtigungsgruppe' : 'Information'} +
++ {message || "Sie sind keiner Berechtigungsgruppe zugewiesen. Bitte kontaktieren Sie einen Administrator, um Zugriff auf die Anwendung zu erhalten."} +
+Lade Daten...
)}Verwalten Sie Berechtigungsgruppen und weisen Sie Spaces zu
+Lade Berechtigungsgruppen...
++ Noch keine Berechtigungsgruppen vorhanden +
++ Erstellen Sie Ihre erste Gruppe, um Berechtigungen zu verwalten +
+ +{group.description}
+ )} ++ 📁 + Zugewiesene Spaces ({group.spaceIds.length}) +
++ Erstellt: {group.createdAt ? new Date(group.createdAt).toLocaleDateString('de-DE', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) : 'Unbekannt'} +
++ Möchten Sie die Berechtigungsgruppe {groupToDelete.name} wirklich löschen? +
++ Diese Aktion kann nicht rückgängig gemacht werden. +
+ + +Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden
+ )}Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden
+ )}+ Verwalten Sie Ihre SSL-Zertifikats-Provider und deren Konfiguration. +
+ + {loading ? ( +Lade Provider...
++ {provider.description} +
+{setting.description}
+ )} + {setting.type === 'password' ? ( + handleConfigChange(setting.name, e.target.value)} + className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-300" + placeholder={setting.label} + required={setting.required} + /> + ) : ( + handleConfigChange(setting.name, e.target.value)} + className="w-full px-4 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-300" + placeholder={setting.label} + required={setting.required} + /> + )} ++ Dieser Provider benötigt keine Konfiguration. +
+ )} ++ {testResult.success ? '✅' : '❌'} {testResult.message} +
+