diff --git a/backend/main.go b/backend/main.go index c089a11..a86dee6 100644 --- a/backend/main.go +++ b/backend/main.go @@ -5,9 +5,9 @@ import ( "crypto/x509" "database/sql" "encoding/asn1" + "encoding/base64" "encoding/hex" "encoding/json" - "encoding/base64" "encoding/pem" "fmt" "io" @@ -258,25 +258,63 @@ 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"` + 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"` + 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"` + GroupIDs []string `json:"groupIds,omitempty"` } // MessageResponse struct für einfache Nachrichten @@ -318,7 +356,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 { @@ -570,10 +608,75 @@ 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 @@ -699,11 +802,11 @@ func getStatsHandler(w http.ResponseWriter, r *http.Request) { } response := StatsResponse{ - Spaces: spacesCount, - FQDNs: fqdnsCount, - CSRs: csrsCount, + Spaces: spacesCount, + FQDNs: fqdnsCount, + CSRs: csrsCount, Certificates: certificatesCount, - Users: usersCount, + Users: usersCount, } json.NewEncoder(w).Encode(response) @@ -2410,9 +2513,10 @@ 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() + // Lade alle Benutzer rows, err := db.QueryContext(ctx, "SELECT id, username, email, created_at FROM users ORDER BY created_at DESC") if err != nil { http.Error(w, "Fehler beim Abrufen der Benutzer", http.StatusInternalServerError) @@ -2422,6 +2526,7 @@ 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) @@ -2430,7 +2535,42 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Fehler beim Lesen der Benutzerdaten: %v", err) return } + user.GroupIDs = []string{} // Initialisiere als leeres Array users = append(users, user) + userIDs = append(userIDs, user.ID) + } + rows.Close() + + // Lade alle Gruppen-Zuweisungen in einem einzigen Query + if len(userIDs) > 0 { + // Erstelle Platzhalter für IN-Clause + placeholders := make([]string, len(userIDs)) + args := make([]interface{}, len(userIDs)) + for i, id := range userIDs { + placeholders[i] = "?" + args[i] = id + } + + query := fmt.Sprintf("SELECT user_id, group_id FROM user_groups WHERE user_id IN (%s)", strings.Join(placeholders, ",")) + groupRows, err := db.QueryContext(ctx, query, args...) + if err == nil { + // Erstelle eine Map von user_id zu group_ids + groupMap := make(map[string][]string) + for groupRows.Next() { + var userID, groupID string + if err := groupRows.Scan(&userID, &groupID); err == nil { + groupMap[userID] = append(groupMap[userID], groupID) + } + } + groupRows.Close() + + // Weise Gruppen-IDs den Benutzern zu + for i := range users { + if groupIDs, exists := groupMap[users[i].ID]; exists { + users[i].GroupIDs = groupIDs + } + } + } } json.NewEncoder(w).Encode(users) @@ -2450,7 +2590,7 @@ 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 @@ -2466,6 +2606,22 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) { return } + // Lade Gruppen-IDs für diesen Benutzer + groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID) + if err == nil { + var groupIDs []string + for groupRows.Next() { + var groupID string + if err := groupRows.Scan(&groupID); err == nil { + groupIDs = append(groupIDs, groupID) + } + } + groupRows.Close() + user.GroupIDs = groupIDs + } else { + user.GroupIDs = []string{} // Initialisiere als leeres Array bei Fehler + } + json.NewEncoder(w).Encode(user) } @@ -2511,7 +2667,7 @@ 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() _, err = db.ExecContext(ctx, @@ -2531,11 +2687,27 @@ 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, CreatedAt: createdAt, + GroupIDs: req.GroupIDs, } w.WriteHeader(http.StatusCreated) @@ -2547,6 +2719,7 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) { auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, 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) } @@ -2571,7 +2744,7 @@ 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 @@ -2659,6 +2832,28 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) { return } + // Aktualisiere Gruppen-Zuweisungen, falls angegeben + if req.GroupIDs != nil { + // 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). @@ -2669,10 +2864,28 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) { return } + // Lade Gruppen-IDs + 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 + } + } + json.NewEncoder(w).Encode(user) // Audit-Log: User aktualisiert - userID, username := getUserFromRequest(r) + requestUserID, requestUsername := getUserFromRequest(r) ipAddress, userAgent := getRequestInfo(r) details := map[string]interface{}{} if req.Username != "" { @@ -2684,7 +2897,10 @@ 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.GroupIDs != nil { + details["groupIds"] = req.GroupIDs + } + auditService.Track(r.Context(), "UPDATE", "user", vars["id"], requestUserID, requestUsername, details, ipAddress, userAgent) } func deleteUserHandler(w http.ResponseWriter, r *http.Request) { @@ -2701,7 +2917,7 @@ 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() result, err := db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", userID) @@ -2921,10 +3137,400 @@ func getAvatarHandler(w http.ResponseWriter, r *http.Request) { io.Copy(w, file) } +// Permission Groups Handler Functions + +func getPermissionGroupsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + // Lade alle Gruppen + rows, err := db.QueryContext(ctx, "SELECT id, name, description, permission, created_at FROM permission_groups ORDER BY created_at DESC") + if err != nil { + http.Error(w, "Fehler beim Abrufen der Berechtigungsgruppen", http.StatusInternalServerError) + log.Printf("Fehler beim Abrufen der Berechtigungsgruppen: %v", err) + return + } + defer rows.Close() + + var groups []PermissionGroup + var groupIDs []string + for rows.Next() { + var group PermissionGroup + err := rows.Scan(&group.ID, &group.Name, &group.Description, &group.Permission, &group.CreatedAt) + if err != nil { + http.Error(w, "Fehler beim Lesen der Gruppen-Daten", http.StatusInternalServerError) + log.Printf("Fehler beim Lesen der Gruppen-Daten: %v", err) + return + } + group.SpaceIDs = []string{} // Initialisiere als leeres Array + groups = append(groups, group) + groupIDs = append(groupIDs, group.ID) + } + rows.Close() + + // Lade alle Space-Zuweisungen in einem einzigen Query + if len(groupIDs) > 0 { + // Erstelle Platzhalter für IN-Clause + placeholders := make([]string, len(groupIDs)) + args := make([]interface{}, len(groupIDs)) + for i, id := range groupIDs { + placeholders[i] = "?" + args[i] = id + } + + query := fmt.Sprintf("SELECT group_id, space_id FROM group_spaces WHERE group_id IN (%s)", strings.Join(placeholders, ",")) + spaceRows, err := db.QueryContext(ctx, query, args...) + if err == nil { + // Erstelle eine Map von group_id zu space_ids + spaceMap := make(map[string][]string) + for spaceRows.Next() { + var groupID, spaceID string + if err := spaceRows.Scan(&groupID, &spaceID); err == nil { + spaceMap[groupID] = append(spaceMap[groupID], spaceID) + } + } + spaceRows.Close() + + // Weise Space-IDs den Gruppen zu + for i := range groups { + if spaceIDs, exists := spaceMap[groups[i].ID]; exists { + groups[i].SpaceIDs = spaceIDs + } + } + } + } + + json.NewEncoder(w).Encode(groups) +} + +func getPermissionGroupHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + vars := mux.Vars(r) + groupID := vars["id"] + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + var group PermissionGroup + err := db.QueryRowContext(ctx, "SELECT id, name, description, permission, created_at FROM permission_groups WHERE id = ?", groupID). + Scan(&group.ID, &group.Name, &group.Description, &group.Permission, &group.CreatedAt) + if err != nil { + if err == sql.ErrNoRows { + http.Error(w, "Berechtigungsgruppe nicht gefunden", http.StatusNotFound) + return + } + http.Error(w, "Fehler beim Abrufen der Berechtigungsgruppe", http.StatusInternalServerError) + log.Printf("Fehler beim Abrufen der Berechtigungsgruppe: %v", err) + return + } + + // Lade Space-IDs für diese Gruppe + spaceRows, err := db.QueryContext(ctx, "SELECT space_id FROM group_spaces WHERE group_id = ?", groupID) + if err == nil { + var spaceIDs []string + for spaceRows.Next() { + var spaceID string + if err := spaceRows.Scan(&spaceID); err == nil { + spaceIDs = append(spaceIDs, spaceID) + } + } + spaceRows.Close() + group.SpaceIDs = spaceIDs + } + + json.NewEncoder(w).Encode(group) +} + +func createPermissionGroupHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + var req CreatePermissionGroupRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Ungültige Anfrage", http.StatusBadRequest) + return + } + + // Validierung + if req.Name == "" { + http.Error(w, "Name ist erforderlich", http.StatusBadRequest) + return + } + + // Validiere Berechtigungsstufe + if req.Permission != PermissionRead && req.Permission != PermissionReadWrite && req.Permission != PermissionFullAccess { + http.Error(w, "Ungültige Berechtigungsstufe. Erlaubt: READ, READ_WRITE, FULL_ACCESS", http.StatusBadRequest) + return + } + + // Erstelle Gruppe + groupID := uuid.New().String() + createdAt := time.Now().Format(time.RFC3339) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + _, err := db.ExecContext(ctx, + "INSERT INTO permission_groups (id, name, description, permission, created_at) VALUES (?, ?, ?, ?, ?)", + groupID, req.Name, req.Description, string(req.Permission), createdAt) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + http.Error(w, "Gruppenname bereits vergeben", http.StatusConflict) + return + } + http.Error(w, "Fehler beim Erstellen der Berechtigungsgruppe", http.StatusInternalServerError) + log.Printf("Fehler beim Erstellen der Berechtigungsgruppe: %v", err) + return + } + + // Weise Spaces zu, falls angegeben + if len(req.SpaceIDs) > 0 { + for _, spaceID := range req.SpaceIDs { + // Prüfe ob Space existiert + var exists bool + err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists) + if err == nil && exists { + _, err = db.ExecContext(ctx, "INSERT OR IGNORE INTO group_spaces (group_id, space_id) VALUES (?, ?)", groupID, spaceID) + if err != nil { + log.Printf("Fehler beim Zuweisen des Space %s zur Gruppe: %v", spaceID, err) + } + } + } + } + + group := PermissionGroup{ + ID: groupID, + Name: req.Name, + Description: req.Description, + Permission: req.Permission, + SpaceIDs: req.SpaceIDs, + CreatedAt: createdAt, + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(group) + + // Audit-Log + requestUserID, requestUsername := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "CREATE", "permission_group", groupID, requestUserID, requestUsername, map[string]interface{}{ + "name": req.Name, + "permission": req.Permission, + "spaceIds": req.SpaceIDs, + "message": fmt.Sprintf("Berechtigungsgruppe erstellt: %s", req.Name), + }, ipAddress, userAgent) +} + +func updatePermissionGroupHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "PUT, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + vars := mux.Vars(r) + groupID := vars["id"] + + var req UpdatePermissionGroupRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Ungültige Anfrage", http.StatusBadRequest) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + // Prüfe ob Gruppe existiert + var exists bool + err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM permission_groups WHERE id = ?)", groupID).Scan(&exists) + if err != nil || !exists { + http.Error(w, "Berechtigungsgruppe nicht gefunden", http.StatusNotFound) + return + } + + // Validiere Berechtigungsstufe, falls angegeben + if req.Permission != "" { + if req.Permission != PermissionRead && req.Permission != PermissionReadWrite && req.Permission != PermissionFullAccess { + http.Error(w, "Ungültige Berechtigungsstufe. Erlaubt: READ, READ_WRITE, FULL_ACCESS", http.StatusBadRequest) + return + } + } + + // Update Felder + updates := []string{} + args := []interface{}{} + + if req.Name != "" { + updates = append(updates, "name = ?") + args = append(args, req.Name) + } + if req.Description != "" { + updates = append(updates, "description = ?") + args = append(args, req.Description) + } + if req.Permission != "" { + updates = append(updates, "permission = ?") + args = append(args, string(req.Permission)) + } + + if len(updates) > 0 { + args = append(args, groupID) + query := fmt.Sprintf("UPDATE permission_groups SET %s WHERE id = ?", strings.Join(updates, ", ")) + _, err = db.ExecContext(ctx, query, args...) + if err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + http.Error(w, "Gruppenname bereits vergeben", http.StatusConflict) + return + } + http.Error(w, "Fehler beim Aktualisieren der Berechtigungsgruppe", http.StatusInternalServerError) + log.Printf("Fehler beim Aktualisieren der Berechtigungsgruppe: %v", err) + return + } + } + + // Aktualisiere Space-Zuweisungen, falls angegeben + if req.SpaceIDs != nil { + // Lösche alle bestehenden Space-Zuweisungen + _, err = db.ExecContext(ctx, "DELETE FROM group_spaces WHERE group_id = ?", groupID) + if err != nil { + log.Printf("Fehler beim Löschen der Space-Zuweisungen: %v", err) + } + + // Füge neue Space-Zuweisungen hinzu + for _, spaceID := range req.SpaceIDs { + // Prüfe ob Space existiert + var exists bool + err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM spaces WHERE id = ?)", spaceID).Scan(&exists) + if err == nil && exists { + _, err = db.ExecContext(ctx, "INSERT INTO group_spaces (group_id, space_id) VALUES (?, ?)", groupID, spaceID) + if err != nil { + log.Printf("Fehler beim Zuweisen des Space %s zur Gruppe: %v", spaceID, err) + } + } + } + } + + // Lade aktualisierte Gruppe + var group PermissionGroup + err = db.QueryRowContext(ctx, "SELECT id, name, description, permission, created_at FROM permission_groups WHERE id = ?", groupID). + Scan(&group.ID, &group.Name, &group.Description, &group.Permission, &group.CreatedAt) + if err != nil { + http.Error(w, "Fehler beim Abrufen der aktualisierten Gruppe", http.StatusInternalServerError) + log.Printf("Fehler beim Abrufen der aktualisierten Gruppe: %v", err) + return + } + + // Lade Space-IDs + if req.SpaceIDs != nil { + group.SpaceIDs = req.SpaceIDs + } else { + spaceRows, err := db.QueryContext(ctx, "SELECT space_id FROM group_spaces WHERE group_id = ?", groupID) + if err == nil { + var spaceIDs []string + for spaceRows.Next() { + var spaceID string + if err := spaceRows.Scan(&spaceID); err == nil { + spaceIDs = append(spaceIDs, spaceID) + } + } + spaceRows.Close() + group.SpaceIDs = spaceIDs + } + } + + json.NewEncoder(w).Encode(group) + + // Audit-Log + requestUserID, requestUsername := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "UPDATE", "permission_group", groupID, requestUserID, requestUsername, map[string]interface{}{ + "name": req.Name, + "permission": req.Permission, + "spaceIds": req.SpaceIDs, + "message": fmt.Sprintf("Berechtigungsgruppe aktualisiert: %s", groupID), + }, ipAddress, userAgent) +} + +func deletePermissionGroupHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + vars := mux.Vars(r) + groupID := vars["id"] + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + result, err := db.ExecContext(ctx, "DELETE FROM permission_groups WHERE id = ?", groupID) + if err != nil { + http.Error(w, "Fehler beim Löschen der Berechtigungsgruppe", http.StatusInternalServerError) + log.Printf("Fehler beim Löschen der Berechtigungsgruppe: %v", err) + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) + return + } + + if rowsAffected == 0 { + http.Error(w, "Berechtigungsgruppe nicht gefunden", http.StatusNotFound) + return + } + + response := MessageResponse{Message: "Berechtigungsgruppe erfolgreich gelöscht"} + json.NewEncoder(w).Encode(response) + + // Audit-Log + requestUserID, requestUsername := getUserFromRequest(r) + ipAddress, userAgent := getRequestInfo(r) + auditService.Track(r.Context(), "DELETE", "permission_group", groupID, requestUserID, requestUsername, map[string]interface{}{ + "message": fmt.Sprintf("Berechtigungsgruppe gelöscht: %s", groupID), + }, ipAddress, userAgent) +} + // Passwortvalidierung nach Richtlinien func validatePassword(password string) error { if len(password) < 8 { - return fmt.Errorf("Passwort muss mindestens 8 Zeichen lang sein") + return fmt.Errorf("passwort muss mindestens 8 Zeichen lang sein") } hasUpper := false @@ -2963,7 +3569,7 @@ func validatePassword(password string) error { } if len(missing) > 0 { - return fmt.Errorf("Passwort muss enthalten: %s", strings.Join(missing, ", ")) + return fmt.Errorf("passwort muss enthalten: %s", strings.Join(missing, ", ")) } return nil @@ -2990,12 +3596,16 @@ 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) 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 } @@ -3014,13 +3624,13 @@ func getRequestInfo(r *http.Request) (ipAddress, userAgent string) { // Hole User-Agent userAgent = r.Header.Get("User-Agent") - + // Prüfe, ob es sich um einen API-Aufruf handelt // API-Aufrufe haben entweder keinen User-Agent oder einen speziellen Header if userAgent == "" || r.Header.Get("X-API-Request") == "true" || r.Header.Get("X-Request-Source") == "api" { userAgent = "API" } - + return ipAddress, userAgent } @@ -3099,7 +3709,7 @@ func getAuditLogsHandler(w http.ResponseWriter, r *http.Request) { // Hole Logs - Verwende Prepared Statement für bessere Kompatibilität var querySQL string var queryArgs []interface{} - + if whereSQL != "" { querySQL = fmt.Sprintf(` SELECT id, timestamp, user_id, username, action, resource_type, resource_id, details, ip_address, user_agent @@ -3123,7 +3733,7 @@ func getAuditLogsHandler(w http.ResponseWriter, r *http.Request) { queryCtx, queryCancel := context.WithTimeout(context.Background(), time.Second*10) defer queryCancel() // Cancel wird erst aufgerufen, wenn die Funktion beendet ist - + rows, err := db.QueryContext(queryCtx, querySQL, queryArgs...) if err != nil { http.Error(w, "Fehler beim Abrufen der Logs", http.StatusInternalServerError) @@ -3201,11 +3811,11 @@ func getAuditLogsHandler(w http.ResponseWriter, r *http.Request) { log.Printf("Zeilen gelesen: %d, Scan-Fehler: %d, Logs hinzugefügt: %d", rowCount, scanErrors, len(logs)) response := map[string]interface{}{ - "logs": logs, - "total": totalCount, - "limit": limit, - "offset": offset, - "hasMore": offset+limit < totalCount, + "logs": logs, + "total": totalCount, + "limit": limit, + "offset": offset, + "hasMore": offset+limit < totalCount, } log.Printf("Audit-Logs abgerufen: %d Einträge gefunden (Total: %d, Limit: %d, Offset: %d)", len(logs), totalCount, limit, offset) @@ -3275,14 +3885,14 @@ func createTestAuditLogHandler(w http.ResponseWriter, r *http.Request) { } var req struct { - Action string `json:"action"` - Entity string `json:"entity"` - EntityID string `json:"entityID"` - UserID string `json:"userID"` - Username string `json:"username"` - Details map[string]interface{} `json:"details"` - IPAddress string `json:"ipAddress"` - UserAgent string `json:"userAgent"` + Action string `json:"action"` + Entity string `json:"entity"` + EntityID string `json:"entityID"` + UserID string `json:"userID"` + Username string `json:"username"` + Details map[string]interface{} `json:"details"` + IPAddress string `json:"ipAddress"` + UserAgent string `json:"userAgent"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -3303,7 +3913,7 @@ func createTestAuditLogHandler(w http.ResponseWriter, r *http.Request) { // Erstelle Context für die Anfrage ctx := r.Context() - + // Track das Event auditService.Track(ctx, req.Action, req.Entity, req.EntityID, req.UserID, req.Username, req.Details, req.IPAddress, req.UserAgent) @@ -3383,7 +3993,7 @@ 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 @@ -3474,7 +4084,7 @@ 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 @@ -3518,7 +4128,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { func main() { log.Println("Starte certigo-addon Backend...") - + // Initialisiere Datenbank log.Println("Initialisiere Datenbank...") initDB() @@ -3542,11 +4152,11 @@ 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") @@ -3571,6 +4181,13 @@ func main() { api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(getAvatarHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(uploadAvatarHandler)).Methods("POST", "OPTIONS") + // Permission Groups Routes (Protected) + api.HandleFunc("/permission-groups", basicAuthMiddleware(getPermissionGroupsHandler)).Methods("GET", "OPTIONS") + api.HandleFunc("/permission-groups", basicAuthMiddleware(createPermissionGroupHandler)).Methods("POST", "OPTIONS") + api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(getPermissionGroupHandler)).Methods("GET", "OPTIONS") + api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(updatePermissionGroupHandler)).Methods("PUT", "OPTIONS") + api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(deletePermissionGroupHandler)).Methods("DELETE", "OPTIONS") + // Provider Routes (Protected) api.HandleFunc("/providers", basicAuthMiddleware(getProvidersHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/providers/{id}", basicAuthMiddleware(getProviderHandler)).Methods("GET", "OPTIONS") @@ -3965,12 +4582,12 @@ func signCSRHandler(w http.ResponseWriter, r *http.Request) { 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) } diff --git a/backend/spaces.db-shm b/backend/spaces.db-shm index d9f70fb..aa0ec1d 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..dc681d3 100644 Binary files a/backend/spaces.db-wal and b/backend/spaces.db-wal differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5764df8..8f216b4 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -9,6 +9,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' @@ -71,6 +72,7 @@ const AppContent = () => { } /> } /> } /> + } /> } /> diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index dfc92f2..2087766 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -22,6 +22,7 @@ const Sidebar = ({ isOpen, setIsOpen }) => { path: '/settings', subItems: [ { path: '/settings/users', label: 'User', icon: '👥' }, + { path: '/settings/permissions', label: 'Berechtigungen', icon: '🔐' }, ] } diff --git a/frontend/src/pages/Permissions.jsx b/frontend/src/pages/Permissions.jsx new file mode 100644 index 0000000..9c556b2 --- /dev/null +++ b/frontend/src/pages/Permissions.jsx @@ -0,0 +1,550 @@ +import { useState, useEffect } from 'react' +import { useAuth } from '../contexts/AuthContext' + +const Permissions = () => { + const { authFetch } = useAuth() + 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 [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) + } 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 = async (groupId) => { + if (!window.confirm('Möchten Sie diese Berechtigungsgruppe wirklich löschen?')) { + return + } + + try { + const response = await authFetch(`/api/permission-groups/${groupId}`, { + method: 'DELETE', + }) + + if (response.ok) { + await fetchGroups() + } else { + const errorData = await response.json() + alert(errorData.error || 'Fehler beim Löschen der Berechtigungsgruppe') + } + } catch (err) { + alert('Fehler beim Löschen der Berechtigungsgruppe') + console.error('Error deleting permission group:', err) + } + } + + 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'} +

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