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 : } +// Admin Only Route Component +const AdminRoute = ({ children }) => { + const { isAuthenticated, loading } = useAuth() + const { isAdmin, loading: permissionsLoading } = usePermissions() + + if (loading || permissionsLoading) { + return ( +
+
+ + + + +

Lade...

+
+
+ ) + } + + if (!isAuthenticated) { + return + } + + if (!isAdmin) { + return ( +
+
+

Zugriff verweigert

+

Nur Administratoren haben Zugriff auf diese Seite.

+
+
+ ) + } + + return children +} + +// Group Required Route Component - User muss einer Berechtigungsgruppe zugewiesen sein +const GroupRequiredRoute = ({ children }) => { + const { isAuthenticated, loading } = useAuth() + const { isAdmin, hasFullAccess, accessibleSpaces, loading: permissionsLoading } = usePermissions() + + if (loading || permissionsLoading) { + return ( +
+
+ + + + +

Lade...

+
+
+ ) + } + + if (!isAuthenticated) { + return + } + + // Admin oder User mit Gruppen haben Zugriff + const hasGroups = isAdmin || hasFullAccess || (accessibleSpaces && accessibleSpaces.length > 0) + + if (!hasGroups) { + return ( + + ) + } + + return children +} + // Public Route Component (redirects to home if already logged in) const PublicRoute = ({ children }) => { const { isAuthenticated, loading } = useAuth() @@ -66,12 +148,14 @@ const AppContent = () => { } /> } /> - } /> - } /> - } /> + } /> + } /> + } /> } /> - } /> - } /> + } /> + } /> + } /> + } />