diff --git a/backend/main.go b/backend/main.go index 28c51ca..ab497b4 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" @@ -200,6 +200,7 @@ type StatsResponse struct { FQDNs int `json:"fqdns"` CSRs int `json:"csrs"` Certificates int `json:"certificates"` + Users int `json:"users"` } type Space struct { @@ -257,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 @@ -317,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 { @@ -496,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 );` @@ -511,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 := ` @@ -569,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")) @@ -600,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 { @@ -608,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") } @@ -655,7 +827,7 @@ func getStatsHandler(w http.ResponseWriter, r *http.Request) { return } - var spacesCount, fqdnsCount, csrsCount, certificatesCount int + var spacesCount, fqdnsCount, csrsCount, certificatesCount, usersCount int // Zähle Spaces err := db.QueryRow("SELECT COUNT(*) FROM spaces").Scan(&spacesCount) @@ -689,11 +861,20 @@ func getStatsHandler(w http.ResponseWriter, r *http.Request) { return } + // Zähle Benutzer + err = db.QueryRow("SELECT COUNT(*) FROM users").Scan(&usersCount) + if err != nil { + http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) + log.Printf("Fehler beim Zählen der Benutzer: %v", err) + return + } + response := StatsResponse{ - Spaces: spacesCount, - FQDNs: fqdnsCount, - CSRs: csrsCount, + Spaces: spacesCount, + FQDNs: fqdnsCount, + CSRs: csrsCount, Certificates: certificatesCount, + Users: usersCount, } json.NewEncoder(w).Encode(response) @@ -710,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) @@ -775,6 +975,33 @@ 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 der Benutzer FULL_ACCESS hat (ohne Space-Beschränkung) + permissions, err := getUserPermissions(userID) + if err != nil || len(permissions.Groups) == 0 { + http.Error(w, "Keine Berechtigung zum Erstellen von Spaces", http.StatusForbidden) + return + } + + hasFullAccess := false + 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) @@ -791,7 +1018,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, ) @@ -813,7 +1040,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, @@ -842,9 +1068,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) @@ -927,7 +1172,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), @@ -958,8 +1202,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) @@ -988,9 +1251,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) @@ -1062,9 +1344,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) @@ -1128,7 +1429,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, @@ -1158,9 +1458,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) @@ -1224,7 +1543,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, @@ -1251,9 +1569,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) @@ -1325,6 +1662,32 @@ 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 + } + + permissions, err := getUserPermissions(userID) + if err != nil || len(permissions.Groups) == 0 { + http.Error(w, "Keine Berechtigung zum Löschen aller FQDNs. Vollzugriff erforderlich.", http.StatusForbidden) + return + } + + hasFullAccess := false + 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" { @@ -1334,7 +1697,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) @@ -1410,6 +1773,32 @@ 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 + } + + permissions, err := getUserPermissions(userID) + if err != nil || len(permissions.Groups) == 0 { + http.Error(w, "Keine Berechtigung zum Löschen aller CSRs. Vollzugriff erforderlich.", http.StatusForbidden) + return + } + + hasFullAccess := false + 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" { @@ -1419,7 +1808,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) @@ -1502,6 +1891,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) @@ -1648,7 +2056,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, @@ -1677,6 +2084,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" @@ -2211,6 +2637,12 @@ components: csrs: type: integer example: 7 + certificates: + type: integer + example: 8 + users: + type: integer + example: 3 Space: type: object properties: @@ -2383,6 +2815,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", "*") @@ -2394,10 +2970,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) @@ -2406,15 +2983,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) @@ -2434,12 +3050,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) @@ -2449,6 +3066,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) } @@ -2495,12 +3130,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") { @@ -2515,11 +3159,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) @@ -2528,11 +3190,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) { @@ -2555,29 +3223,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 == "" { @@ -2643,20 +3391,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 != "" { @@ -2668,7 +3477,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) { @@ -2685,9 +3513,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) @@ -2710,9 +3561,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) } @@ -2905,10 +3756,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 @@ -2947,7 +4188,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 @@ -2974,18 +4215,331 @@ 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, hat er keinen Zugriff + if 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, hat er keine Berechtigung + if 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 + } + + permissions, err := getUserPermissions(userID) + if err != nil { + return []string{}, err + } + + // Wenn der Benutzer keine Gruppen hat, hat er keinen Zugriff + 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 @@ -2998,13 +4552,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 } @@ -3083,7 +4637,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 @@ -3107,7 +4661,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) @@ -3185,11 +4739,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) @@ -3259,14 +4813,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 { @@ -3287,7 +4841,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) @@ -3367,11 +4921,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 { @@ -3389,6 +4944,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 { @@ -3406,6 +4972,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") @@ -3458,13 +5055,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) @@ -3478,6 +5076,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 { @@ -3502,7 +5108,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { func main() { log.Println("Starte certigo-addon Backend...") - + // Initialisiere Datenbank log.Println("Initialisiere Datenbank...") initDB() @@ -3526,34 +5132,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") @@ -3844,6 +5458,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 @@ -3862,7 +5495,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 = ? @@ -3946,15 +5579,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) } @@ -3973,6 +5605,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 @@ -4028,9 +5679,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..99be2a6 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..06c95e9 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..8ef5d4c 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,7 @@ 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 Login from './pages/Login' import AuditLogs from './pages/AuditLogs' @@ -33,6 +35,43 @@ 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 +} + // Public Route Component (redirects to home if already logged in) const PublicRoute = ({ children }) => { const { isAuthenticated, loading } = useAuth() @@ -70,7 +109,8 @@ const AppContent = () => { } /> } /> } /> - } /> + } /> + } /> } /> @@ -85,7 +125,9 @@ function App() { return ( - + + + ) diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index dfc92f2..52ccb4f 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -1,11 +1,13 @@ import { Link, useLocation, useNavigate } from 'react-router-dom' import { useAuth } from '../contexts/AuthContext' +import { usePermissions } from '../contexts/PermissionsContext' import { useState, useEffect } from 'react' const Sidebar = ({ isOpen, setIsOpen }) => { const location = useLocation() const navigate = useNavigate() const { user, logout } = useAuth() + const { isAdmin } = usePermissions() const [expandedMenus, setExpandedMenus] = useState({}) const menuItems = [ @@ -22,6 +24,7 @@ const Sidebar = ({ isOpen, setIsOpen }) => { path: '/settings', subItems: [ { path: '/settings/users', label: 'User', icon: '👥' }, + { path: '/settings/permissions', label: 'Berechtigungen', icon: '🔐' }, ] } @@ -127,7 +130,8 @@ const Sidebar = ({ isOpen, setIsOpen }) => { ))} - {/* Settings Menu mit Unterpunkten */} + {/* Settings Menu mit Unterpunkten - nur für Admins */} + {isAdmin && (
  • + )} {/* Profil-Eintrag und Logout am unteren Ende */}
    diff --git a/frontend/src/contexts/PermissionsContext.jsx b/frontend/src/contexts/PermissionsContext.jsx new file mode 100644 index 0000000..fb1e399 --- /dev/null +++ b/frontend/src/contexts/PermissionsContext.jsx @@ -0,0 +1,102 @@ +import { createContext, useContext, useState, useEffect, useCallback } from 'react' +import { useAuth } from './AuthContext' + +const PermissionsContext = createContext(null) + +export const PermissionsProvider = ({ children }) => { + const { authFetch, isAuthenticated } = useAuth() + const [permissions, setPermissions] = useState({ + isAdmin: false, + hasFullAccess: false, + accessibleSpaces: [], + canCreateSpace: false, + canDeleteSpace: false, + canCreateFqdn: {}, + canDeleteFqdn: {}, + canUploadCSR: {}, + canSignCSR: {}, + }) + const [loading, setLoading] = useState(true) + + const fetchPermissions = useCallback(async () => { + if (!isAuthenticated) { + setLoading(false) + return + } + + try { + setLoading(true) + const response = await authFetch('/api/user/permissions') + if (response.ok) { + const data = await response.json() + setPermissions({ + isAdmin: data.isAdmin || false, + hasFullAccess: data.hasFullAccess || false, + accessibleSpaces: data.accessibleSpaces || [], + canCreateSpace: data.permissions?.canCreateSpace || false, + canDeleteSpace: data.permissions?.canDeleteSpace || false, + canCreateFqdn: data.permissions?.canCreateFqdn || {}, + canDeleteFqdn: data.permissions?.canDeleteFqdn || {}, + canUploadCSR: data.permissions?.canUploadCSR || {}, + canSignCSR: data.permissions?.canSignCSR || {}, + }) + } + } catch (err) { + console.error('Error fetching permissions:', err) + } finally { + setLoading(false) + } + }, [isAuthenticated, authFetch]) + + useEffect(() => { + if (isAuthenticated) { + fetchPermissions() + } else { + setPermissions({ + isAdmin: false, + hasFullAccess: false, + accessibleSpaces: [], + canCreateSpace: false, + canDeleteSpace: false, + canCreateFqdn: {}, + canDeleteFqdn: {}, + canUploadCSR: {}, + canSignCSR: {}, + }) + setLoading(false) + } + }, [isAuthenticated, fetchPermissions]) + + const canCreateSpace = () => permissions.canCreateSpace + const canDeleteSpace = (spaceId) => permissions.canDeleteSpace + const canCreateFqdn = (spaceId) => permissions.canCreateFqdn[spaceId] === true + const canDeleteFqdn = (spaceId) => permissions.canDeleteFqdn[spaceId] === true + const canUploadCSR = (spaceId) => permissions.canUploadCSR[spaceId] === true + const canSignCSR = (spaceId) => permissions.canSignCSR[spaceId] === true + const hasAccessToSpace = (spaceId) => permissions.accessibleSpaces.includes(spaceId) + + const value = { + permissions, + loading, + refreshPermissions: fetchPermissions, + isAdmin: permissions.isAdmin, + canCreateSpace, + canDeleteSpace, + canCreateFqdn, + canDeleteFqdn, + canUploadCSR, + canSignCSR, + hasAccessToSpace, + } + + return {children} +} + +export const usePermissions = () => { + const context = useContext(PermissionsContext) + if (!context) { + throw new Error('usePermissions muss innerhalb eines PermissionsProvider verwendet werden') + } + return context +} + diff --git a/frontend/src/hooks/usePermissions.js b/frontend/src/hooks/usePermissions.js new file mode 100644 index 0000000..24ea552 --- /dev/null +++ b/frontend/src/hooks/usePermissions.js @@ -0,0 +1,3 @@ +// Re-export from PermissionsContext for backward compatibility +export { usePermissions } from '../contexts/PermissionsContext' + diff --git a/frontend/src/pages/AuditLogs.jsx b/frontend/src/pages/AuditLogs.jsx index 16da40d..4ae55c7 100644 --- a/frontend/src/pages/AuditLogs.jsx +++ b/frontend/src/pages/AuditLogs.jsx @@ -163,6 +163,7 @@ const AuditLogs = () => { csr: 'CSR', provider: 'Provider', certificate: 'Zertifikat', + permission_group: 'Berechtigungsgruppen', } const toggleLogExpansion = (logId) => { @@ -239,6 +240,7 @@ const AuditLogs = () => { +
    diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index dd79380..2cef111 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -267,6 +267,20 @@ const Home = () => {
    + +
    +
    +
    +

    Benutzer

    +

    {stats.users || 0}

    +
    +
    + + + +
    +
    +
    ) : (

    Fehler beim Laden der Statistiken

    diff --git a/frontend/src/pages/Permissions.jsx b/frontend/src/pages/Permissions.jsx new file mode 100644 index 0000000..b7bec68 --- /dev/null +++ b/frontend/src/pages/Permissions.jsx @@ -0,0 +1,637 @@ +import { useState, useEffect } from 'react' +import { useAuth } from '../contexts/AuthContext' +import { usePermissions } from '../hooks/usePermissions' + +const Permissions = () => { + const { authFetch } = useAuth() + const { refreshPermissions } = usePermissions() + const [groups, setGroups] = useState([]) + const [spaces, setSpaces] = useState([]) + const [loading, setLoading] = useState(false) + const [fetching, setFetching] = useState(true) + const [error, setError] = useState('') + const [showForm, setShowForm] = useState(false) + const [editingGroup, setEditingGroup] = useState(null) + const [showDeleteModal, setShowDeleteModal] = useState(false) + const [groupToDelete, setGroupToDelete] = useState(null) + const [confirmChecked, setConfirmChecked] = useState(false) + const [formData, setFormData] = useState({ + name: '', + description: '', + permission: 'READ', + spaceIds: [] + }) + + useEffect(() => { + // Lade Daten parallel beim Mount und beim Zurückkehren zur Seite + let isMounted = true + + const loadData = async () => { + if (isMounted) { + setFetching(true) + } + await Promise.all([fetchGroups(), fetchSpaces()]) + } + + loadData() + + return () => { + isMounted = false + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const fetchGroups = async () => { + try { + setError('') + const response = await authFetch('/api/permission-groups') + if (response.ok) { + const data = await response.json() + setGroups(Array.isArray(data) ? data : []) + } else { + setError('Fehler beim Abrufen der Berechtigungsgruppen') + } + } catch (err) { + setError('Fehler beim Abrufen der Berechtigungsgruppen') + console.error('Error fetching permission groups:', err) + } finally { + setFetching(false) + } + } + + const fetchSpaces = async () => { + try { + const response = await authFetch('/api/spaces') + if (response.ok) { + const data = await response.json() + setSpaces(Array.isArray(data) ? data : []) + } + } catch (err) { + console.error('Error fetching spaces:', err) + } + } + + const handleSubmit = async (e) => { + e.preventDefault() + setError('') + setLoading(true) + + if (!formData.name.trim()) { + setError('Bitte geben Sie einen Namen ein') + setLoading(false) + return + } + + if (!formData.permission) { + setError('Bitte wählen Sie eine Berechtigungsstufe') + setLoading(false) + return + } + + try { + const url = editingGroup + ? `/api/permission-groups/${editingGroup.id}` + : '/api/permission-groups' + const method = editingGroup ? 'PUT' : 'POST' + + const body = { + name: formData.name, + description: formData.description, + permission: formData.permission, + spaceIds: formData.spaceIds + } + + const response = await authFetch(url, { + method, + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) + + if (response.ok) { + await fetchGroups() + setFormData({ name: '', description: '', permission: 'READ', spaceIds: [] }) + setShowForm(false) + setEditingGroup(null) + // Aktualisiere Berechtigungen nach Änderung an Berechtigungsgruppen + refreshPermissions() + } else { + const errorData = await response.json() + setError(errorData.error || 'Fehler beim Speichern der Berechtigungsgruppe') + } + } catch (err) { + setError('Fehler beim Speichern der Berechtigungsgruppe') + console.error('Error saving permission group:', err) + } finally { + setLoading(false) + } + } + + const handleEdit = (group) => { + setEditingGroup(group) + setFormData({ + name: group.name, + description: group.description || '', + permission: group.permission, + spaceIds: group.spaceIds || [] + }) + setShowForm(true) + } + + const handleDelete = (group) => { + setGroupToDelete(group) + setShowDeleteModal(true) + setConfirmChecked(false) + } + + const confirmDelete = async () => { + if (!confirmChecked || !groupToDelete) { + return + } + + try { + const response = await authFetch(`/api/permission-groups/${groupToDelete.id}`, { + method: 'DELETE', + }) + + if (response.ok) { + await fetchGroups() + setShowDeleteModal(false) + setGroupToDelete(null) + setConfirmChecked(false) + // Aktualisiere Berechtigungen nach Löschen einer Berechtigungsgruppe + refreshPermissions() + } else { + const errorData = await response.json().catch(() => ({ error: 'Fehler beim Löschen' })) + alert(errorData.error || 'Fehler beim Löschen der Berechtigungsgruppe') + } + } catch (err) { + console.error('Error deleting permission group:', err) + alert('Fehler beim Löschen der Berechtigungsgruppe') + } + } + + const cancelDelete = () => { + setShowDeleteModal(false) + setGroupToDelete(null) + setConfirmChecked(false) + } + + const handleChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value + }) + } + + const handleSpaceToggle = (spaceId) => { + setFormData(prev => { + const spaceIds = prev.spaceIds || [] + if (spaceIds.includes(spaceId)) { + return { ...prev, spaceIds: spaceIds.filter(id => id !== spaceId) } + } else { + return { ...prev, spaceIds: [...spaceIds, spaceId] } + } + }) + } + + const getPermissionLabel = (permission) => { + switch (permission) { + case 'READ': + return 'Lesen' + case 'READ_WRITE': + return 'Lesen/Schreiben' + case 'FULL_ACCESS': + return 'Vollzugriff' + default: + return permission + } + } + + const getPermissionBadgeColor = (permission) => { + switch (permission) { + case 'READ': + return 'bg-green-600/20 text-green-300 border-green-500/30' + case 'READ_WRITE': + return 'bg-yellow-600/20 text-yellow-300 border-yellow-500/30' + case 'FULL_ACCESS': + return 'bg-purple-600/20 text-purple-300 border-purple-500/30' + default: + return 'bg-blue-600/20 text-blue-300 border-blue-500/30' + } + } + + const getPermissionIcon = (permission) => { + switch (permission) { + case 'READ': + return '👁️' + case 'READ_WRITE': + return '✏️' + case 'FULL_ACCESS': + return '🔓' + default: + return '🔐' + } + } + + const getPermissionDescription = (permission) => { + switch (permission) { + case 'READ': + return 'Nur CSRs und Zertifikate ansehen. Keine Requests, keine Lösch-/Erstellrechte.' + case 'READ_WRITE': + return 'FQDNs innerhalb eines Spaces erstellen (nicht löschen), CSRs requesten und ansehen. Keine Spaces löschen/erstellen.' + case 'FULL_ACCESS': + return 'Vollzugriff: Alles darf gemacht werden. Löschen, Erstellen, CSR requesten und ansehen.' + default: + return '' + } + } + + return ( +
    +
    +
    +
    +

    + 🔐 + Berechtigungsgruppen +

    +

    Verwalten Sie Berechtigungsgruppen und weisen Sie Spaces zu

    +
    + +
    + + {error && ( +
    + {error} +
    + )} + + {showForm && ( +
    +
    +

    + {editingGroup ? '✏️' : '➕'} + {editingGroup ? 'Berechtigungsgruppe bearbeiten' : 'Neue Berechtigungsgruppe'} +

    + +
    +
    +
    +
    + + +
    + +
    + +