package main import ( "context" "crypto/x509" "database/sql" "encoding/asn1" "encoding/base64" "encoding/hex" "encoding/json" "encoding/pem" "fmt" "io" "log" "net/http" "os" "path/filepath" "strconv" "strings" "time" "github.com/google/uuid" "github.com/gorilla/mux" _ "github.com/mattn/go-sqlite3" "golang.org/x/crypto/bcrypt" "certigo-addon-backend/internal/core" "certigo-addon-backend/providers" ) // OID zu Name Mapping (OpenSSL Format) var oidToName = map[string]string{ "2.5.29.15": "X509v3 Key Usage", "2.5.29.17": "X509v3 Subject Alternative Name", "2.5.29.19": "X509v3 Basic Constraints", "2.5.29.31": "X509v3 CRL Distribution Points", "2.5.29.32": "X509v3 Certificate Policies", "2.5.29.35": "X509v3 Authority Key Identifier", "2.5.29.37": "X509v3 Extended Key Usage", "2.5.29.14": "X509v3 Subject Key Identifier", "1.3.6.1.5.5.7.1.1": "Authority Information Access", "1.3.6.1.5.5.7.48.1": "OCSP", "1.3.6.1.5.5.7.48.2": "CA Issuers", } // Extended Key Usage OIDs (OpenSSL Format) var extendedKeyUsageOIDs = map[string]string{ "1.3.6.1.5.5.7.3.1": "TLS Web Server Authentication", "1.3.6.1.5.5.7.3.2": "TLS Web Client Authentication", "1.3.6.1.5.5.7.3.3": "Code Signing", "1.3.6.1.5.5.7.3.4": "E-mail Protection", "1.3.6.1.5.5.7.3.8": "Time Stamping", "1.3.6.1.5.5.7.3.9": "OCSP Signing", "1.3.6.1.5.5.7.3.5": "IPsec End System", "1.3.6.1.5.5.7.3.6": "IPsec Tunnel", "1.3.6.1.5.5.7.3.7": "IPsec User", } // Key Usage Flags var keyUsageFlags = map[int]string{ 0: "Digital Signature", 1: "Content Commitment", 2: "Key Encipherment", 3: "Data Encipherment", 4: "Key Agreement", 5: "Key Cert Sign", 6: "CRL Sign", 7: "Encipher Only", 8: "Decipher Only", } func getExtensionName(oid string) string { if name, ok := oidToName[oid]; ok { return name } return "Unknown Extension" } func parseExtensionValue(oid string, value []byte, csr *x509.CertificateRequest) (string, []string) { switch oid { case "2.5.29.37": // Extended Key Usage return parseExtendedKeyUsage(value) case "2.5.29.15": // Key Usage return parseKeyUsage(value) case "2.5.29.19": // Basic Constraints return parseBasicConstraints(value) case "2.5.29.17": // Subject Alternative Name return parseSubjectAlternativeName(csr) default: return hex.EncodeToString(value), nil } } func parseSubjectAlternativeName(csr *x509.CertificateRequest) (string, []string) { var parts []string // DNS Names for _, dns := range csr.DNSNames { parts = append(parts, fmt.Sprintf("DNS:%s", dns)) } // Email Addresses for _, email := range csr.EmailAddresses { parts = append(parts, fmt.Sprintf("email:%s", email)) } // IP Addresses for _, ip := range csr.IPAddresses { parts = append(parts, fmt.Sprintf("IP:%s", ip.String())) } // URIs for _, uri := range csr.URIs { parts = append(parts, fmt.Sprintf("URI:%s", uri.String())) } if len(parts) > 0 { return strings.Join(parts, ", "), parts } return "No Subject Alternative Name", nil } func parseExtendedKeyUsage(value []byte) (string, []string) { var oids []asn1.ObjectIdentifier _, err := asn1.Unmarshal(value, &oids) if err != nil { return hex.EncodeToString(value), nil } var purposes []string for _, oid := range oids { oidStr := oid.String() if purpose, ok := extendedKeyUsageOIDs[oidStr]; ok { purposes = append(purposes, purpose) } else { purposes = append(purposes, oidStr) } } if len(purposes) > 0 { // Format wie OpenSSL: jede Purpose auf eigener Zeile return strings.Join(purposes, "\n "), purposes } return hex.EncodeToString(value), nil } func parseKeyUsage(value []byte) (string, []string) { var bits asn1.BitString _, err := asn1.Unmarshal(value, &bits) if err != nil { return hex.EncodeToString(value), nil } var usages []string for i := 0; i < len(bits.Bytes)*8 && i < 9; i++ { if bits.At(i) == 1 { if usage, ok := keyUsageFlags[i]; ok { usages = append(usages, usage) } } } if len(usages) > 0 { return strings.Join(usages, ", "), usages } return "No key usage specified", nil } func parseBasicConstraints(value []byte) (string, []string) { var constraints struct { IsCA bool `asn1:"optional"` MaxPathLen int `asn1:"optional,default:-1"` } _, err := asn1.Unmarshal(value, &constraints) if err != nil { return hex.EncodeToString(value), nil } var parts []string if constraints.IsCA { parts = append(parts, "CA: true") } else { parts = append(parts, "CA: false") } if constraints.MaxPathLen >= 0 { parts = append(parts, fmt.Sprintf("Path Length: %d", constraints.MaxPathLen)) } return strings.Join(parts, ", "), parts } type HealthResponse struct { Status string `json:"status"` Message string `json:"message"` Time string `json:"time"` } type StatsResponse struct { Spaces int `json:"spaces"` FQDNs int `json:"fqdns"` CSRs int `json:"csrs"` Certificates int `json:"certificates"` Users int `json:"users"` } type Space struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` CreatedAt string `json:"createdAt"` } type CreateSpaceRequest struct { Name string `json:"name"` Description string `json:"description"` } type FQDN struct { ID string `json:"id"` SpaceID string `json:"spaceId"` FQDN string `json:"fqdn"` Description string `json:"description"` CreatedAt string `json:"createdAt"` } type CreateFQDNRequest struct { FQDN string `json:"fqdn"` Description string `json:"description"` } type Extension struct { ID string `json:"id"` OID string `json:"oid"` Name string `json:"name"` Critical bool `json:"critical"` Value string `json:"value"` Description string `json:"description"` Purposes []string `json:"purposes,omitempty"` } type CSR struct { ID string `json:"id"` FQDNID string `json:"fqdnId"` SpaceID string `json:"spaceId"` FQDN string `json:"fqdn"` CSRPEM string `json:"csrPem"` Subject string `json:"subject"` PublicKeyAlgorithm string `json:"publicKeyAlgorithm"` SignatureAlgorithm string `json:"signatureAlgorithm"` KeySize int `json:"keySize"` DNSNames []string `json:"dnsNames"` EmailAddresses []string `json:"emailAddresses"` IPAddresses []string `json:"ipAddresses"` URIs []string `json:"uris"` Extensions []Extension `json:"extensions"` CreatedAt string `json:"createdAt"` } // User struct für Benutzer type User struct { 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"` } // 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"` 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 type MessageResponse struct { Message string `json:"message"` } // AuditLog struct für Audit-Logs type AuditLog struct { ID string `json:"id"` Timestamp string `json:"timestamp"` UserID string `json:"userId,omitempty"` Username string `json:"username,omitempty"` Action string `json:"action"` ResourceType string `json:"resourceType"` ResourceID string `json:"resourceId,omitempty"` Details string `json:"details,omitempty"` IPAddress string `json:"ipAddress,omitempty"` UserAgent string `json:"userAgent,omitempty"` } var db *sql.DB var auditService *core.AuditService func initDB() { var err error // SQLite Connection String mit Timeout und WAL Mode für bessere Concurrency // _busy_timeout erhöht die Wartezeit bei Locks db, err = sql.Open("sqlite3", "./spaces.db?_foreign_keys=1&_journal_mode=WAL&_timeout=10000&_busy_timeout=10000") if err != nil { log.Fatal("Fehler beim Öffnen der Datenbank:", err) } // Setze Connection Pool Settings db.SetMaxOpenConns(1) // SQLite unterstützt nur eine Verbindung gleichzeitig db.SetMaxIdleConns(1) // Teste die Verbindung mit Retry log.Println("Teste Datenbank-Verbindung...") maxRetries := 5 for i := 0; i < maxRetries; i++ { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) err := db.PingContext(ctx) cancel() if err != nil { if i < maxRetries-1 { log.Printf("Datenbank-Verbindung fehlgeschlagen, versuche erneut (%d/%d)...", i+1, maxRetries) time.Sleep(time.Second * 2) continue } log.Fatal("Fehler beim Verbinden mit der Datenbank nach mehreren Versuchen:", err) } log.Println("Datenbank-Verbindung erfolgreich") break } // Aktiviere Foreign Keys (auch über Connection String, aber zur Sicherheit nochmal) log.Println("Aktiviere Foreign Keys...") ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "PRAGMA foreign_keys = ON") cancel() if err != nil { log.Fatal("Fehler beim Aktivieren der Foreign Keys:", err) } // Prüfe und bereinige WAL-Dateien falls nötig log.Println("Führe WAL-Checkpoint aus...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "PRAGMA wal_checkpoint(TRUNCATE)") cancel() if err != nil { log.Printf("Warnung: WAL-Checkpoint fehlgeschlagen: %v", err) } // Erstelle Tabelle falls sie nicht existiert log.Println("Erstelle spaces-Tabelle...") createTableSQL := ` CREATE TABLE IF NOT EXISTS spaces ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, created_at DATETIME NOT NULL );` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createTableSQL) cancel() if err != nil { log.Fatal("Fehler beim Erstellen der Tabelle:", err) } // Erstelle FQDN-Tabelle log.Println("Erstelle fqdns-Tabelle...") createFQDNTableSQL := ` CREATE TABLE IF NOT EXISTS fqdns ( id TEXT PRIMARY KEY, space_id TEXT NOT NULL, fqdn TEXT NOT NULL, description TEXT, created_at DATETIME NOT NULL, FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE );` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createFQDNTableSQL) cancel() if err != nil { log.Fatal("Fehler beim Erstellen der FQDN-Tabelle:", err) } // Erstelle CSR-Tabelle log.Println("Erstelle csrs-Tabelle...") createCSRTableSQL := ` CREATE TABLE IF NOT EXISTS csrs ( id TEXT PRIMARY KEY, fqdn_id TEXT NOT NULL, space_id TEXT NOT NULL, fqdn TEXT NOT NULL, csr_pem TEXT NOT NULL, subject TEXT, public_key_algorithm TEXT, signature_algorithm TEXT, key_size INTEGER, dns_names TEXT, email_addresses TEXT, ip_addresses TEXT, uris TEXT, extensions TEXT, created_at DATETIME NOT NULL, FOREIGN KEY (fqdn_id) REFERENCES fqdns(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, createCSRTableSQL) cancel() if err != nil { log.Fatal("Fehler beim Erstellen der CSR-Tabelle:", err) } // Füge Extensions-Spalte hinzu, falls sie nicht existiert (für bestehende Datenbanken) // Prüfe zuerst, ob die Spalte bereits existiert log.Println("Prüfe Extensions-Spalte...") var columnExists bool ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) rows, err := db.QueryContext(ctx, "PRAGMA table_info(csrs)") cancel() if err == nil { defer rows.Close() for rows.Next() { var cid int var name string var dataType string var notNull int var defaultValue interface{} var pk int if err := rows.Scan(&cid, &name, &dataType, ¬Null, &defaultValue, &pk); err == nil { if name == "extensions" { columnExists = true break } } } rows.Close() } // Füge Spalte nur hinzu, wenn sie nicht existiert if !columnExists { log.Println("Füge Extensions-Spalte hinzu...") ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, "ALTER TABLE csrs ADD COLUMN extensions TEXT") cancel() if err != nil { // Ignoriere "duplicate column" Fehler, da die Spalte möglicherweise zwischenzeitlich hinzugefügt wurde if !strings.Contains(err.Error(), "duplicate column") { log.Printf("Fehler beim Hinzufügen der Extensions-Spalte: %v", err) } } else { log.Println("Extensions-Spalte zur csrs-Tabelle hinzugefügt") } } else { log.Println("Extensions-Spalte existiert bereits") } // Erstelle Zertifikat-Tabelle log.Println("Erstelle certificates-Tabelle...") createCertificateTableSQL := ` CREATE TABLE IF NOT EXISTS certificates ( id TEXT PRIMARY KEY, fqdn_id TEXT NOT NULL, space_id TEXT NOT NULL, csr_id TEXT NOT NULL, certificate_id TEXT NOT NULL, provider_id TEXT NOT NULL, certificate_pem TEXT, status TEXT NOT NULL, created_at DATETIME NOT NULL, FOREIGN KEY (fqdn_id) REFERENCES fqdns(id) ON DELETE CASCADE, FOREIGN KEY (space_id) REFERENCES spaces(id) ON DELETE CASCADE, FOREIGN KEY (csr_id) REFERENCES csrs(id) ON DELETE CASCADE );` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createCertificateTableSQL) 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 (z.B. andere go run main.go Instanzen).") } log.Fatal("Fehler beim Erstellen der Zertifikat-Tabelle:", err) } // Erstelle Users-Tabelle log.Println("Erstelle users-Tabelle...") createUsersTableSQL := ` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, 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 );` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createUsersTableSQL) 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 (z.B. andere go run main.go Instanzen).") } log.Fatal("Fehler beim Erstellen der Users-Tabelle:", err) } 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 := ` CREATE TABLE IF NOT EXISTS audit_logs ( id TEXT PRIMARY KEY, timestamp DATETIME NOT NULL, user_id TEXT, username TEXT, action TEXT NOT NULL, resource_type TEXT NOT NULL, resource_id TEXT, details TEXT, ip_address TEXT, user_agent TEXT );` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createAuditLogsTableSQL) 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 (z.B. andere go run main.go Instanzen).") } log.Fatal("Fehler beim Erstellen der Audit-Log-Tabelle:", err) } // Erstelle Index für bessere Performance log.Println("Erstelle Indizes für audit_logs...") createIndexSQL := ` CREATE INDEX IF NOT EXISTS idx_audit_logs_timestamp ON audit_logs(timestamp DESC); CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action); CREATE INDEX IF NOT EXISTS idx_audit_logs_resource_type ON audit_logs(resource_type);` ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) _, err = db.ExecContext(ctx, createIndexSQL) cancel() if err != nil { log.Printf("Warnung: Fehler beim Erstellen der Indizes: %v", err) } // Erstelle Default Admin-User falls nicht vorhanden createDefaultAdmin() // Initialisiere AuditService (muss nach DB-Initialisierung passieren) auditService = core.NewAuditService(db) if auditService == nil { log.Fatal("Fehler: AuditService konnte nicht initialisiert werden") } log.Println("AuditService erfolgreich initialisiert") // Erstelle Upload-Ordner für Profilbilder avatarDir := "uploads/avatars" if err := os.MkdirAll(avatarDir, 0755); err != nil { log.Printf("Warnung: Konnte Avatar-Ordner nicht erstellen: %v", err) } 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*10) defer cancel() // Prüfe ob bereits ein Admin-User mit UID "admin" existiert var count int 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 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 id = 'admin'").Scan(&storedHash) if err == nil { // Teste ob das Passwort "admin" ist testErr := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte("admin")) if testErr == nil { log.Println("Admin-User Passwort ist korrekt gesetzt") } else { log.Println("Warnung: Admin-User Passwort ist nicht 'admin'") } } return } // 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 { log.Printf("Fehler beim Hashen des Admin-Passworts: %v", err) return } adminID := "admin" // Feste UID statt UUID createdAt := time.Now().Format(time.RFC3339) _, err = db.ExecContext(ctx, "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: UID='admin', username='admin', password='admin'") log.Printf(" User ID: %s", adminID) log.Printf(" Email: admin@certigo.local") } func healthHandler(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 } response := HealthResponse{ Status: "ok", Message: "Backend ist erreichbar", Time: time.Now().Format(time.RFC3339), } json.NewEncoder(w).Encode(response) } func getStatsHandler(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 } var spacesCount, fqdnsCount, csrsCount, certificatesCount, usersCount int // Zähle Spaces err := db.QueryRow("SELECT COUNT(*) FROM spaces").Scan(&spacesCount) if err != nil { http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der Spaces: %v", err) return } // Zähle FQDNs err = db.QueryRow("SELECT COUNT(*) FROM fqdns").Scan(&fqdnsCount) if err != nil { http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der FQDNs: %v", err) return } // Zähle CSRs err = db.QueryRow("SELECT COUNT(*) FROM csrs").Scan(&csrsCount) if err != nil { http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der CSRs: %v", err) return } // Zähle Zertifikate err = db.QueryRow("SELECT COUNT(*) FROM certificates").Scan(&certificatesCount) if err != nil { http.Error(w, "Fehler beim Abrufen der Statistiken", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der Zertifikate: %v", err) 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, Certificates: certificatesCount, Users: usersCount, } json.NewEncoder(w).Encode(response) } func getSpacesHandler(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) // Hole alle Spaces, auf die der Benutzer Zugriff hat accessibleSpaceIDs, err := getAccessibleSpaceIDs(userID) if err != nil { http.Error(w, "Fehler beim Prüfen der Berechtigungen", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der Berechtigungen: %v", err) return } // 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) return } defer rows.Close() spaces := make([]Space, 0) for rows.Next() { var space Space var createdAt time.Time var description sql.NullString err := rows.Scan(&space.ID, &space.Name, &description, &createdAt) if err != nil { http.Error(w, "Fehler beim Lesen der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Lesen der Daten: %v", err) return } if description.Valid { space.Description = description.String } else { space.Description = "" } space.CreatedAt = createdAt.Format(time.RFC3339) spaces = append(spaces, space) } if err = rows.Err(); err != nil { http.Error(w, "Fehler beim Verarbeiten der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Verarbeiten der Daten: %v", err) return } // Stelle sicher, dass immer ein Array zurückgegeben wird if spaces == nil { spaces = []Space{} } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(spaces) } func createSpaceHandler(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 } // Prüfe Berechtigung: Nur FULL_ACCESS darf Spaces erstellen userID, username := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } // Prüfe ob User Admin ist - Admins haben immer Vollzugriff isAdmin, err := isUserAdmin(userID) if err != nil { log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) return } // Prüfe, ob der Benutzer FULL_ACCESS hat (ohne Space-Beschränkung) permissions, err := getUserPermissions(userID) if err != nil { http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err) return } // Admin oder HasFullAccess erlaubt Space-Erstellung hasFullAccess := isAdmin || permissions.HasFullAccess // Wenn nicht Admin, prüfe auch Gruppen if !isAdmin && len(permissions.Groups) > 0 { for _, group := range permissions.Groups { if group.Permission == PermissionFullAccess { hasFullAccess = true break } } } if !hasFullAccess { http.Error(w, "Keine Berechtigung zum Erstellen von Spaces. Vollzugriff erforderlich.", http.StatusForbidden) return } var req CreateSpaceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.Name == "" { http.Error(w, "Name is required", http.StatusBadRequest) return } // Generiere eindeutige UUID id := uuid.New().String() createdAt := time.Now() // Speichere in Datenbank _, err = db.Exec( "INSERT INTO spaces (id, name, description, created_at) VALUES (?, ?, ?, ?)", id, req.Name, req.Description, createdAt, ) if err != nil { http.Error(w, "Fehler beim Speichern des Space", http.StatusInternalServerError) log.Printf("Fehler beim Speichern des Space: %v", err) return } newSpace := Space{ ID: id, Name: req.Name, Description: req.Description, CreatedAt: createdAt.Format(time.RFC3339), } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(newSpace) // Audit-Log: Space erstellt if auditService != nil { ipAddress, userAgent := getRequestInfo(r) auditService.Track(r.Context(), "CREATE", "space", id, userID, username, map[string]interface{}{ "name": req.Name, "description": req.Description, "message": fmt.Sprintf("Space erstellt: %s", req.Name), }, ipAddress, userAgent) } } func deleteSpaceHandler(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) id := vars["id"] if id == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) 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) if err != nil { http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des Space: %v", err) return } if !exists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } // Prüfe ob FQDNs vorhanden sind var fqdnCount int err = db.QueryRow("SELECT COUNT(*) FROM fqdns WHERE space_id = ?", id).Scan(&fqdnCount) if err != nil { http.Error(w, "Fehler beim Prüfen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der FQDNs: %v", err) return } // Prüfe Query-Parameter für Mitlöschen deleteFqdns := r.URL.Query().Get("deleteFqdns") == "true" if fqdnCount > 0 && !deleteFqdns { http.Error(w, "Space enthält noch FQDNs. Bitte löschen Sie zuerst die FQDNs oder wählen Sie die Option zum Mitlöschen.", http.StatusConflict) return } // Beginne Transaktion für atomares Löschen tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche FQDNs zuerst, wenn gewünscht if deleteFqdns && fqdnCount > 0 { _, err = tx.Exec("DELETE FROM fqdns WHERE space_id = ?", id) if err != nil { http.Error(w, "Fehler beim Löschen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen der FQDNs: %v", err) return } log.Printf("Gelöscht: %d FQDNs für Space %s", fqdnCount, id) } // Lösche den Space result, err := tx.Exec("DELETE FROM spaces WHERE id = ?", id) if err != nil { http.Error(w, "Fehler beim Löschen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Löschen des Space: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der gelöschten Zeilen: %v", err) return } if rowsAffected == 0 { tx.Rollback() http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"message": "Space erfolgreich gelöscht"}) // Audit-Log: Space gelöscht ipAddress, userAgent := getRequestInfo(r) details := map[string]interface{}{ "message": fmt.Sprintf("Space gelöscht: %s", id), } if deleteFqdns && fqdnCount > 0 { details["fqdnsDeleted"] = fqdnCount details["message"] = fmt.Sprintf("Space gelöscht: %s (mit %d FQDNs)", id, fqdnCount) } auditService.Track(r.Context(), "DELETE", "space", id, userID, username, details, ipAddress, userAgent) } func getSpaceFqdnCountHandler(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) spaceID := vars["id"] if spaceID == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) 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) if err != nil { http.Error(w, "Fehler beim Abrufen der FQDN-Anzahl", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der FQDN-Anzahl: %v", err) return } json.NewEncoder(w).Encode(map[string]int{"count": count}) } func getFqdnsHandler(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) spaceID := vars["id"] if spaceID == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) 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) if err != nil { http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des Space: %v", err) return } if !exists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } rows, err := db.Query("SELECT id, space_id, fqdn, description, created_at FROM fqdns WHERE space_id = ? ORDER BY created_at DESC", spaceID) if err != nil { http.Error(w, "Fehler beim Abrufen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der FQDNs: %v", err) return } defer rows.Close() var fqdns []FQDN for rows.Next() { var fqdn FQDN var createdAt time.Time var description sql.NullString err := rows.Scan(&fqdn.ID, &fqdn.SpaceID, &fqdn.FQDN, &description, &createdAt) if err != nil { http.Error(w, "Fehler beim Lesen der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Lesen der Daten: %v", err) return } if description.Valid { fqdn.Description = description.String } else { fqdn.Description = "" } fqdn.CreatedAt = createdAt.Format(time.RFC3339) fqdns = append(fqdns, fqdn) } if err = rows.Err(); err != nil { http.Error(w, "Fehler beim Verarbeiten der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Verarbeiten der Daten: %v", err) return } if fqdns == nil { fqdns = []FQDN{} } json.NewEncoder(w).Encode(fqdns) } func createFqdnHandler(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 } vars := mux.Vars(r) spaceID := vars["id"] if spaceID == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) 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) if err != nil { http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des Space: %v", err) return } if !exists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } var req CreateFQDNRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.FQDN == "" { http.Error(w, "FQDN is required", http.StatusBadRequest) return } // Prüfe ob der FQDN bereits existiert (case-insensitive) var fqdnExists bool err = db.QueryRow("SELECT EXISTS(SELECT 1 FROM fqdns WHERE LOWER(fqdn) = LOWER(?))", req.FQDN).Scan(&fqdnExists) if err != nil { http.Error(w, "Fehler beim Prüfen des FQDN", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des FQDN: %v", err) return } if fqdnExists { http.Error(w, "Dieser FQDN existiert bereits", http.StatusConflict) return } // Generiere eindeutige UUID id := uuid.New().String() createdAt := time.Now() // Speichere in Datenbank _, err = db.Exec( "INSERT INTO fqdns (id, space_id, fqdn, description, created_at) VALUES (?, ?, ?, ?, ?)", id, spaceID, req.FQDN, req.Description, createdAt, ) if err != nil { http.Error(w, "Fehler beim Speichern des FQDN", http.StatusInternalServerError) log.Printf("Fehler beim Speichern des FQDN: %v", err) return } newFqdn := FQDN{ ID: id, SpaceID: spaceID, FQDN: req.FQDN, Description: req.Description, CreatedAt: createdAt.Format(time.RFC3339), } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(newFqdn) // Audit-Log: FQDN erstellt ipAddress, userAgent := getRequestInfo(r) auditService.Track(r.Context(), "CREATE", "fqdn", id, userID, username, map[string]interface{}{ "fqdn": req.FQDN, "spaceId": spaceID, "description": req.Description, "message": fmt.Sprintf("FQDN erstellt: %s (Space: %s)", req.FQDN, spaceID), }, ipAddress, userAgent) } func deleteFqdnHandler(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) spaceID := vars["id"] fqdnID := vars["fqdnId"] if spaceID == "" || fqdnID == "" { http.Error(w, "Space ID und FQDN ID sind erforderlich", http.StatusBadRequest) 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) if err != nil { http.Error(w, "Fehler beim Prüfen des FQDN", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des FQDN: %v", err) return } if !exists { http.Error(w, "FQDN nicht gefunden", http.StatusNotFound) return } // Beginne Transaktion für atomares Löschen tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche zuerst alle CSRs für diesen FQDN (falls CASCADE nicht funktioniert) _, err = tx.Exec("DELETE FROM csrs WHERE fqdn_id = ? AND space_id = ?", fqdnID, spaceID) if err != nil { http.Error(w, "Fehler beim Löschen der CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen der CSRs: %v", err) return } // Lösche den FQDN result, err := tx.Exec("DELETE FROM fqdns WHERE id = ? AND space_id = ?", fqdnID, spaceID) if err != nil { http.Error(w, "Fehler beim Löschen des FQDN", http.StatusInternalServerError) log.Printf("Fehler beim Löschen des FQDN: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der gelöschten Zeilen: %v", err) return } if rowsAffected == 0 { tx.Rollback() http.Error(w, "FQDN nicht gefunden", http.StatusNotFound) return } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } log.Printf("FQDN %s und zugehörige CSRs erfolgreich gelöscht", fqdnID) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"message": "FQDN erfolgreich gelöscht"}) // Audit-Log: FQDN gelöscht ipAddress, userAgent := getRequestInfo(r) auditService.Track(r.Context(), "DELETE", "fqdn", fqdnID, userID, username, map[string]interface{}{ "spaceId": spaceID, "message": fmt.Sprintf("FQDN gelöscht (Space: %s)", spaceID), }, ipAddress, userAgent) } func deleteAllFqdnsHandler(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) spaceID := vars["id"] if spaceID == "" { http.Error(w, "Space ID ist erforderlich", http.StatusBadRequest) 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) if err != nil { http.Error(w, "Fehler beim Prüfen des Space", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen des Space: %v", err) return } if !exists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } // Beginne Transaktion für atomares Löschen tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche zuerst alle CSRs für alle FQDNs dieses Spaces _, err = tx.Exec("DELETE FROM csrs WHERE space_id = ?", spaceID) if err != nil { http.Error(w, "Fehler beim Löschen der CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen der CSRs: %v", err) return } // Lösche alle FQDNs des Spaces result, err := tx.Exec("DELETE FROM fqdns WHERE space_id = ?", spaceID) if err != nil { http.Error(w, "Fehler beim Löschen der FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen der FQDNs: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der gelöschten Zeilen: %v", err) return } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } log.Printf("Gelöscht: %d FQDNs und zugehörige CSRs aus Space %s", rowsAffected, spaceID) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Alle FQDNs und zugehörige CSRs erfolgreich gelöscht", "deletedCount": rowsAffected, }) } func deleteAllFqdnsGlobalHandler(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 } // Prüfe Berechtigung: Nur FULL_ACCESS darf alle FQDNs global löschen userID, _ := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } // Prüfe ob User Admin ist - Admins haben immer Vollzugriff isAdmin, err := isUserAdmin(userID) if err != nil { log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) return } permissions, err := getUserPermissions(userID) if err != nil { http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err) return } // Admin oder HasFullAccess erlaubt Löschen aller FQDNs hasFullAccess := isAdmin || permissions.HasFullAccess // Wenn nicht Admin, prüfe auch Gruppen if !isAdmin && len(permissions.Groups) > 0 { for _, group := range permissions.Groups { if group.Permission == PermissionFullAccess { hasFullAccess = true break } } } if !hasFullAccess { http.Error(w, "Keine Berechtigung zum Löschen aller FQDNs. Vollzugriff erforderlich.", http.StatusForbidden) return } // Prüfe Query-Parameter für Bestätigung (Sicherheitsmaßnahme) confirm := r.URL.Query().Get("confirm") if confirm != "true" { http.Error(w, "Bestätigung erforderlich. Verwenden Sie ?confirm=true", http.StatusBadRequest) return } // Zähle zuerst die Anzahl der FQDNs var totalCount int 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) return } if totalCount == 0 { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Keine FQDNs zum Löschen vorhanden", "deletedCount": 0, }) return } // Beginne Transaktion für atomares Löschen tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche zuerst alle CSRs _, err = tx.Exec("DELETE FROM csrs") if err != nil { http.Error(w, "Fehler beim Löschen aller CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen aller CSRs: %v", err) return } // Lösche alle FQDNs result, err := tx.Exec("DELETE FROM fqdns") if err != nil { http.Error(w, "Fehler beim Löschen aller FQDNs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen aller FQDNs: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der gelöschten Zeilen: %v", err) return } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } log.Printf("Gelöscht: %d FQDNs und alle zugehörigen CSRs aus allen Spaces", rowsAffected) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Alle FQDNs und zugehörige CSRs erfolgreich gelöscht", "deletedCount": rowsAffected, }) } func deleteAllCSRsHandler(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 } // Prüfe Berechtigung: Nur FULL_ACCESS darf alle CSRs löschen userID, _ := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } // Prüfe ob User Admin ist - Admins haben immer Vollzugriff isAdmin, err := isUserAdmin(userID) if err != nil { log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) http.Error(w, "Fehler beim Prüfen der Berechtigung", http.StatusInternalServerError) return } permissions, err := getUserPermissions(userID) if err != nil { http.Error(w, "Fehler beim Abrufen der Berechtigungen", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der Berechtigungen: %v", err) return } // Admin oder HasFullAccess erlaubt Löschen aller CSRs hasFullAccess := isAdmin || permissions.HasFullAccess // Wenn nicht Admin, prüfe auch Gruppen if !isAdmin && len(permissions.Groups) > 0 { for _, group := range permissions.Groups { if group.Permission == PermissionFullAccess { hasFullAccess = true break } } } if !hasFullAccess { http.Error(w, "Keine Berechtigung zum Löschen aller CSRs. Vollzugriff erforderlich.", http.StatusForbidden) return } // Prüfe Query-Parameter für Bestätigung (Sicherheitsmaßnahme) confirm := r.URL.Query().Get("confirm") if confirm != "true" { http.Error(w, "Bestätigung erforderlich. Verwenden Sie ?confirm=true", http.StatusBadRequest) return } // Zähle zuerst die Anzahl der CSRs var totalCount int 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) return } if totalCount == 0 { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Keine CSRs zum Löschen vorhanden", "deletedCount": 0, }) return } // Beginne Transaktion für atomare Operation tx, err := db.Begin() if err != nil { http.Error(w, "Fehler beim Starten der Transaktion", http.StatusInternalServerError) log.Printf("Fehler beim Starten der Transaktion: %v", err) return } defer tx.Rollback() // Lösche alle CSRs result, err := tx.Exec("DELETE FROM csrs") if err != nil { http.Error(w, "Fehler beim Löschen aller CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen aller CSRs: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { tx.Rollback() http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der gelöschten Zeilen: %v", err) return } // Committe die Transaktion err = tx.Commit() if err != nil { http.Error(w, "Fehler beim Speichern der Änderungen", http.StatusInternalServerError) log.Printf("Fehler beim Committen der Transaktion: %v", err) return } log.Printf("Gelöscht: %d CSRs aus allen Spaces", rowsAffected) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Alle CSRs erfolgreich gelöscht", "deletedCount": rowsAffected, }) } func uploadCSRHandler(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 } // Parse multipart form err := r.ParseMultipartForm(10 << 20) // 10 MB max if err != nil { http.Error(w, "Fehler beim Parsen des Formulars", http.StatusBadRequest) return } spaceID := r.FormValue("spaceId") fqdnName := r.FormValue("fqdn") if spaceID == "" || fqdnName == "" { http.Error(w, "spaceId und fqdn sind erforderlich", http.StatusBadRequest) 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) if err != nil || !spaceExists { http.Error(w, "Space nicht gefunden", http.StatusNotFound) return } // Prüfe ob FQDN existiert und zum Space gehört var fqdnID string err = db.QueryRow("SELECT id FROM fqdns WHERE fqdn = ? AND space_id = ?", fqdnName, spaceID).Scan(&fqdnID) if err != nil { if err == sql.ErrNoRows { http.Error(w, "FQDN nicht gefunden oder gehört nicht zu diesem Space", http.StatusNotFound) } else { http.Error(w, "Fehler beim Prüfen des FQDN", http.StatusInternalServerError) } return } // Hole die CSR-Datei file, header, err := r.FormFile("csr") if err != nil { http.Error(w, "Fehler beim Lesen der CSR-Datei", http.StatusBadRequest) return } defer file.Close() // Lese den Dateiinhalt csrBytes := make([]byte, header.Size) _, err = io.ReadFull(file, csrBytes) if err != nil { http.Error(w, "Fehler beim Lesen der CSR-Datei", http.StatusBadRequest) return } csrPEM := string(csrBytes) // Parse CSR block, _ := pem.Decode(csrBytes) if block == nil { http.Error(w, "Ungültiges PEM-Format", http.StatusBadRequest) return } csr, err := x509.ParseCertificateRequest(block.Bytes) if err != nil { http.Error(w, "Fehler beim Parsen des CSR: "+err.Error(), http.StatusBadRequest) return } // Extrahiere Informationen subject := csr.Subject.String() publicKeyAlgorithm := csr.PublicKeyAlgorithm.String() signatureAlgorithm := csr.SignatureAlgorithm.String() // Bestimme Key Size keySize := 0 if csr.PublicKey != nil { switch pub := csr.PublicKey.(type) { case interface{ Size() int }: keySize = pub.Size() * 8 // Convert bytes to bits } } // Extrahiere SANs dnsNames := csr.DNSNames emailAddresses := csr.EmailAddresses ipAddresses := make([]string, len(csr.IPAddresses)) for i, ip := range csr.IPAddresses { ipAddresses[i] = ip.String() } uris := make([]string, len(csr.URIs)) for i, uri := range csr.URIs { uris[i] = uri.String() } // Extrahiere Extensions extensions := make([]Extension, 0) for _, ext := range csr.Extensions { oidStr := ext.Id.String() name := getExtensionName(oidStr) description, purposes := parseExtensionValue(oidStr, ext.Value, csr) extension := Extension{ ID: ext.Id.String(), OID: oidStr, Name: name, Critical: ext.Critical, Value: hex.EncodeToString(ext.Value), Description: description, Purposes: purposes, } extensions = append(extensions, extension) } // Konvertiere Slices zu JSON-Strings für DB dnsNamesJSON, _ := json.Marshal(dnsNames) emailAddressesJSON, _ := json.Marshal(emailAddresses) ipAddressesJSON, _ := json.Marshal(ipAddresses) urisJSON, _ := json.Marshal(uris) extensionsJSON, _ := json.Marshal(extensions) // Generiere eindeutige ID csrID := uuid.New().String() createdAt := time.Now() // Speichere in Datenbank _, err = db.Exec(` INSERT INTO csrs ( id, fqdn_id, space_id, fqdn, csr_pem, subject, public_key_algorithm, signature_algorithm, key_size, dns_names, email_addresses, ip_addresses, uris, extensions, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, csrID, fqdnID, spaceID, fqdnName, csrPEM, subject, publicKeyAlgorithm, signatureAlgorithm, keySize, string(dnsNamesJSON), string(emailAddressesJSON), string(ipAddressesJSON), string(urisJSON), string(extensionsJSON), createdAt, ) if err != nil { http.Error(w, "Fehler beim Speichern des CSR", http.StatusInternalServerError) log.Printf("Fehler beim Speichern des CSR: %v", err) return } newCSR := CSR{ ID: csrID, FQDNID: fqdnID, SpaceID: spaceID, FQDN: fqdnName, CSRPEM: csrPEM, Subject: subject, PublicKeyAlgorithm: publicKeyAlgorithm, SignatureAlgorithm: signatureAlgorithm, KeySize: keySize, DNSNames: dnsNames, EmailAddresses: emailAddresses, IPAddresses: ipAddresses, URIs: uris, Extensions: extensions, CreatedAt: createdAt.Format(time.RFC3339), } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(newCSR) // Audit-Log: CSR hochgeladen ipAddress, userAgent := getRequestInfo(r) auditService.Track(r.Context(), "UPLOAD", "csr", csrID, userID, username, map[string]interface{}{ "fqdnId": fqdnID, "spaceId": spaceID, "message": fmt.Sprintf("CSR hochgeladen für FQDN: %s (Space: %s)", fqdnID, spaceID), }, ipAddress, userAgent) } func getCSRByFQDNHandler(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) spaceID := vars["spaceId"] fqdnID := vars["fqdnId"] if spaceID == "" || fqdnID == "" { http.Error(w, "spaceId und fqdnId sind erforderlich", http.StatusBadRequest) 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" if latestOnly { // Hole nur den neuesten CSR var csr CSR var createdAt time.Time var dnsNamesJSON, emailAddressesJSON, ipAddressesJSON, urisJSON, extensionsJSON sql.NullString err := db.QueryRow(` SELECT id, fqdn_id, space_id, fqdn, csr_pem, subject, public_key_algorithm, signature_algorithm, key_size, dns_names, email_addresses, ip_addresses, uris, extensions, created_at FROM csrs WHERE fqdn_id = ? AND space_id = ? ORDER BY created_at DESC LIMIT 1 `, fqdnID, spaceID).Scan( &csr.ID, &csr.FQDNID, &csr.SpaceID, &csr.FQDN, &csr.CSRPEM, &csr.Subject, &csr.PublicKeyAlgorithm, &csr.SignatureAlgorithm, &csr.KeySize, &dnsNamesJSON, &emailAddressesJSON, &ipAddressesJSON, &urisJSON, &extensionsJSON, &createdAt, ) if err != nil { if err == sql.ErrNoRows { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(nil) return } http.Error(w, "Fehler beim Abrufen des CSR", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen des CSR: %v", err) return } // Parse JSON-Strings zurück zu Slices json.Unmarshal([]byte(dnsNamesJSON.String), &csr.DNSNames) json.Unmarshal([]byte(emailAddressesJSON.String), &csr.EmailAddresses) json.Unmarshal([]byte(ipAddressesJSON.String), &csr.IPAddresses) json.Unmarshal([]byte(urisJSON.String), &csr.URIs) if extensionsJSON.Valid { json.Unmarshal([]byte(extensionsJSON.String), &csr.Extensions) } else { csr.Extensions = []Extension{} } csr.CreatedAt = createdAt.Format(time.RFC3339) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(csr) } else { // Hole alle CSRs für diesen FQDN rows, err := db.Query(` SELECT id, fqdn_id, space_id, fqdn, csr_pem, subject, public_key_algorithm, signature_algorithm, key_size, dns_names, email_addresses, ip_addresses, uris, extensions, created_at FROM csrs WHERE fqdn_id = ? AND space_id = ? ORDER BY created_at DESC `, fqdnID, spaceID) if err != nil { http.Error(w, "Fehler beim Abrufen der CSRs", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen der CSRs: %v", err) return } defer rows.Close() var csrs []CSR for rows.Next() { var csr CSR var createdAt time.Time var dnsNamesJSON, emailAddressesJSON, ipAddressesJSON, urisJSON string var extensionsJSON sql.NullString err := rows.Scan( &csr.ID, &csr.FQDNID, &csr.SpaceID, &csr.FQDN, &csr.CSRPEM, &csr.Subject, &csr.PublicKeyAlgorithm, &csr.SignatureAlgorithm, &csr.KeySize, &dnsNamesJSON, &emailAddressesJSON, &ipAddressesJSON, &urisJSON, &extensionsJSON, &createdAt, ) if err != nil { http.Error(w, "Fehler beim Lesen der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Lesen der Daten: %v", err) return } // Parse JSON-Strings zurück zu Slices json.Unmarshal([]byte(dnsNamesJSON), &csr.DNSNames) json.Unmarshal([]byte(emailAddressesJSON), &csr.EmailAddresses) json.Unmarshal([]byte(ipAddressesJSON), &csr.IPAddresses) json.Unmarshal([]byte(urisJSON), &csr.URIs) if extensionsJSON.Valid { json.Unmarshal([]byte(extensionsJSON.String), &csr.Extensions) } else { csr.Extensions = []Extension{} } csr.CreatedAt = createdAt.Format(time.RFC3339) csrs = append(csrs, csr) } if err = rows.Err(); err != nil { http.Error(w, "Fehler beim Verarbeiten der Daten", http.StatusInternalServerError) log.Printf("Fehler beim Verarbeiten der Daten: %v", err) return } if csrs == nil { csrs = []CSR{} } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(csrs) } } func swaggerUIHandler(w http.ResponseWriter, r *http.Request) { html := ` Certigo Addon API - Swagger UI
` w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(html)) } func openAPIHandler(w http.ResponseWriter, r *http.Request) { // Lese die OpenAPI YAML Datei openAPIContent := `openapi: 3.0.3 info: title: Certigo Addon API description: API für die Verwaltung von Spaces, FQDNs und Certificate Signing Requests (CSRs) version: 1.0.0 contact: name: Certigo Addon servers: - url: http://localhost:8080/api description: Local development server paths: /health: get: summary: System Health Check description: Prüft den Systemstatus des Backends tags: [System] responses: '200': description: System ist erreichbar content: application/json: schema: $ref: '#/components/schemas/HealthResponse' /stats: get: summary: Statistiken abrufen description: Ruft Statistiken über die Anzahl der Spaces, FQDNs und CSRs ab tags: [System] responses: '200': description: Statistiken erfolgreich abgerufen content: application/json: schema: $ref: '#/components/schemas/StatsResponse' /spaces: get: summary: Alle Spaces abrufen description: Ruft eine Liste aller Spaces ab tags: [Spaces] responses: '200': description: Liste der Spaces content: application/json: schema: type: array items: $ref: '#/components/schemas/Space' post: summary: Space erstellen description: Erstellt einen neuen Space tags: [Spaces] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateSpaceRequest' responses: '201': description: Space erfolgreich erstellt content: application/json: schema: $ref: '#/components/schemas/Space' '400': description: Ungültige Anfrage /spaces/{id}: delete: summary: Space löschen description: Löscht einen Space. Wenn der Space FQDNs enthält, muss der Parameter deleteFqdns=true gesetzt werden. tags: [Spaces] parameters: - name: id in: path required: true schema: type: string format: uuid - name: deleteFqdns in: query required: false schema: type: boolean default: false description: Wenn true, werden alle FQDNs des Spaces mitgelöscht responses: '200': description: Space erfolgreich gelöscht content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '404': description: Space nicht gefunden '409': description: Space enthält noch FQDNs /spaces/{id}/fqdns/count: get: summary: FQDN-Anzahl abrufen description: Ruft die Anzahl der FQDNs für einen Space ab tags: [FQDNs] parameters: - name: id in: path required: true schema: type: string format: uuid responses: '200': description: Anzahl der FQDNs content: application/json: schema: $ref: '#/components/schemas/CountResponse' /spaces/{id}/fqdns: get: summary: Alle FQDNs eines Spaces abrufen description: Ruft alle FQDNs für einen Space ab tags: [FQDNs] parameters: - name: id in: path required: true schema: type: string format: uuid responses: '200': description: Liste der FQDNs content: application/json: schema: type: array items: $ref: '#/components/schemas/FQDN' '404': description: Space nicht gefunden post: summary: FQDN erstellen description: Erstellt einen neuen FQDN innerhalb eines Spaces tags: [FQDNs] parameters: - name: id in: path required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateFQDNRequest' responses: '201': description: FQDN erfolgreich erstellt content: application/json: schema: $ref: '#/components/schemas/FQDN' '400': description: Ungültige Anfrage '404': description: Space nicht gefunden '409': description: FQDN existiert bereits in diesem Space delete: summary: Alle FQDNs eines Spaces löschen description: Löscht alle FQDNs eines Spaces tags: [FQDNs] parameters: - name: id in: path required: true schema: type: string format: uuid responses: '200': description: Alle FQDNs erfolgreich gelöscht content: application/json: schema: $ref: '#/components/schemas/DeleteResponse' /spaces/{id}/fqdns/{fqdnId}: delete: summary: FQDN löschen description: Löscht einen einzelnen FQDN tags: [FQDNs] parameters: - name: id in: path required: true schema: type: string format: uuid - name: fqdnId in: path required: true schema: type: string format: uuid responses: '200': description: FQDN erfolgreich gelöscht content: application/json: schema: $ref: '#/components/schemas/MessageResponse' '404': description: FQDN nicht gefunden /fqdns: delete: summary: Alle FQDNs global löschen description: Löscht alle FQDNs aus allen Spaces. Erfordert confirm=true Query-Parameter. tags: [FQDNs] parameters: - name: confirm in: query required: true schema: type: boolean description: Muss true sein, um die Operation auszuführen responses: '200': description: Alle FQDNs erfolgreich gelöscht content: application/json: schema: $ref: '#/components/schemas/DeleteResponse' '400': description: Bestätigung erforderlich /csrs: delete: summary: Alle CSRs global löschen description: Löscht alle CSRs aus allen Spaces. Erfordert confirm=true Query-Parameter. tags: [CSRs] parameters: - name: confirm in: query required: true schema: type: string description: Muss "true" sein, um die Operation auszuführen example: "true" responses: '200': description: Alle CSRs erfolgreich gelöscht content: application/json: schema: $ref: '#/components/schemas/DeleteResponse' '400': description: Bestätigung erforderlich /spaces/{spaceId}/fqdns/{fqdnId}/csr: post: summary: CSR hochladen description: Lädt einen CSR (Certificate Signing Request) im PEM-Format hoch tags: [CSRs] parameters: - name: spaceId in: path required: true schema: type: string format: uuid - name: fqdnId in: path required: true schema: type: string format: uuid requestBody: required: true content: multipart/form-data: schema: type: object required: [csr, spaceId, fqdn] properties: csr: type: string format: binary description: CSR-Datei im PEM-Format spaceId: type: string description: ID des Spaces fqdn: type: string description: Name des FQDNs responses: '201': description: CSR erfolgreich hochgeladen content: application/json: schema: $ref: '#/components/schemas/CSR' '400': description: Ungültige Anfrage oder ungültiges CSR-Format '404': description: Space oder FQDN nicht gefunden get: summary: CSR(s) abrufen description: Ruft CSR(s) für einen FQDN ab. Mit latest=true wird nur der neueste CSR zurückgegeben. tags: [CSRs] parameters: - name: spaceId in: path required: true schema: type: string format: uuid - name: fqdnId in: path required: true schema: type: string format: uuid - name: latest in: query required: false schema: type: boolean default: false description: Wenn true, wird nur der neueste CSR zurückgegeben responses: '200': description: CSR(s) erfolgreich abgerufen content: application/json: schema: oneOf: - $ref: '#/components/schemas/CSR' - type: array items: $ref: '#/components/schemas/CSR' '404': description: FQDN nicht gefunden components: schemas: HealthResponse: type: object properties: status: type: string example: "ok" message: type: string example: "Backend ist erreichbar" time: type: string format: date-time example: "2024-01-15T10:30:00Z" StatsResponse: type: object properties: spaces: type: integer example: 5 fqdns: type: integer example: 12 csrs: type: integer example: 7 certificates: type: integer example: 8 users: type: integer example: 3 Space: type: object properties: id: type: string format: uuid example: "550e8400-e29b-41d4-a716-446655440000" name: type: string example: "Mein Space" description: type: string example: "Beschreibung des Spaces" createdAt: type: string format: date-time example: "2024-01-15T10:30:00Z" CreateSpaceRequest: type: object required: [name] properties: name: type: string example: "Mein Space" description: type: string example: "Beschreibung des Spaces" FQDN: type: object properties: id: type: string format: uuid example: "660e8400-e29b-41d4-a716-446655440000" spaceId: type: string format: uuid example: "550e8400-e29b-41d4-a716-446655440000" fqdn: type: string example: "example.com" description: type: string example: "Beschreibung des FQDN" createdAt: type: string format: date-time example: "2024-01-15T10:30:00Z" CreateFQDNRequest: type: object required: [fqdn] properties: fqdn: type: string example: "example.com" description: type: string example: "Beschreibung des FQDN" Extension: type: object properties: id: type: string example: "2.5.29.37" oid: type: string example: "2.5.29.37" name: type: string example: "X509v3 Extended Key Usage" critical: type: boolean example: false value: type: string example: "301406082b0601050507030106082b06010505070302" description: type: string example: "TLS Web Server Authentication" purposes: type: array items: type: string example: ["TLS Web Server Authentication", "TLS Web Client Authentication"] CSR: type: object properties: id: type: string format: uuid example: "770e8400-e29b-41d4-a716-446655440000" fqdnId: type: string format: uuid example: "660e8400-e29b-41d4-a716-446655440000" spaceId: type: string format: uuid example: "550e8400-e29b-41d4-a716-446655440000" fqdn: type: string example: "example.com" csrPem: type: string example: "-----BEGIN CERTIFICATE REQUEST-----" subject: type: string example: "CN=example.com" publicKeyAlgorithm: type: string example: "RSA" signatureAlgorithm: type: string example: "SHA256-RSA" keySize: type: integer example: 2048 dnsNames: type: array items: type: string example: ["example.com", "www.example.com"] emailAddresses: type: array items: type: string example: ["admin@example.com"] ipAddresses: type: array items: type: string example: ["192.168.1.1"] uris: type: array items: type: string example: ["https://example.com"] extensions: type: array items: $ref: '#/components/schemas/Extension' createdAt: type: string format: date-time example: "2024-01-15T10:30:00Z" MessageResponse: type: object properties: message: type: string example: "Operation erfolgreich" CountResponse: type: object properties: count: type: integer example: 5 DeleteResponse: type: object properties: message: type: string example: "Alle FQDNs erfolgreich gelöscht" deletedCount: type: integer example: 5` w.Header().Set("Content-Type", "application/x-yaml") w.Write([]byte(openAPIContent)) } // 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", "*") 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 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) return } defer rows.Close() var users []User var userIDs []string for rows.Next() { var user User 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) } func getUserHandler(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) userID := vars["id"] ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() var user User 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) return } http.Error(w, "Fehler beim Abrufen des Benutzers", http.StatusInternalServerError) 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) } func createUserHandler(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 CreateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Ungültige Anfrage", http.StatusBadRequest) return } // Validierung if req.Username == "" || req.Email == "" || req.Password == "" { http.Error(w, "Benutzername, E-Mail und Passwort sind erforderlich", http.StatusBadRequest) return } // Passwortrichtlinie prüfen if err := validatePassword(req.Password); err != nil { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) return } // Passwort hashen hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { http.Error(w, "Fehler beim Hashen des Passworts", http.StatusInternalServerError) log.Printf("Fehler beim Hashen des Passworts: %v", err) return } // Erstelle Benutzer userID := uuid.New().String() createdAt := time.Now().Format(time.RFC3339) 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, 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") { http.Error(w, "Benutzername bereits vergeben", http.StatusConflict) } else { http.Error(w, "E-Mail-Adresse bereits vergeben", http.StatusConflict) } return } http.Error(w, "Fehler beim Erstellen des Benutzers", http.StatusInternalServerError) log.Printf("Fehler beim Erstellen des Benutzers: %v", err) 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) json.NewEncoder(w).Encode(user) // Audit-Log: User erstellt requestUserID, requestUsername := getUserFromRequest(r) ipAddress, userAgent := getRequestInfo(r) auditDetails := map[string]interface{}{ "username": req.Username, "email": req.Email, "groupIds": req.GroupIDs, "message": fmt.Sprintf("User erstellt: %s (%s)", req.Username, req.Email), } 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) { 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) userID := vars["id"] var req UpdateUserRequest 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 Benutzer existiert 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{}{} // 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 != "" && (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 == "" { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": "Altes Passwort ist erforderlich, um das Passwort zu ändern"}) return } // Hole aktuelles Passwort-Hash aus der Datenbank var storedHash string err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE id = ?", userID).Scan(&storedHash) if err != nil { http.Error(w, "Fehler beim Abrufen des Benutzers", http.StatusInternalServerError) log.Printf("Fehler beim Abrufen des Benutzers: %v", err) return } // Validiere altes Passwort err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(req.OldPassword)) if err != nil { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": "Altes Passwort ist falsch"}) return } // Passwortrichtlinie prüfen if err := validatePassword(req.Password); err != nil { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) return } hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { http.Error(w, "Fehler beim Hashen des Passworts", http.StatusInternalServerError) log.Printf("Fehler beim Hashen des Passworts: %v", err) return } updates = append(updates, "password_hash = ?") args = append(args, string(hashedPassword)) } if len(updates) == 0 { http.Error(w, "Keine Felder zum Aktualisieren", http.StatusBadRequest) return } args = append(args, userID) query := fmt.Sprintf("UPDATE users SET %s WHERE id = ?", strings.Join(updates, ", ")) _, err = db.ExecContext(ctx, query, args...) if err != nil { if strings.Contains(err.Error(), "UNIQUE constraint failed") { if strings.Contains(err.Error(), "username") { http.Error(w, "Benutzername bereits vergeben", http.StatusConflict) } else { http.Error(w, "E-Mail-Adresse bereits vergeben", http.StatusConflict) } return } http.Error(w, "Fehler beim Aktualisieren des Benutzers", http.StatusInternalServerError) log.Printf("Fehler beim Aktualisieren des Benutzers: %v", err) 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 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 requestUserID, requestUsername := getUserFromRequest(r) ipAddress, userAgent := getRequestInfo(r) details := map[string]interface{}{} if req.Username != "" { details["username"] = req.Username } if req.Email != "" { details["email"] = req.Email } if req.Password != "" { details["passwordChanged"] = true } 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) { 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) userID := vars["id"] 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) log.Printf("Fehler beim Löschen des Benutzers: %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, "Benutzer nicht gefunden", http.StatusNotFound) return } response := MessageResponse{Message: "Benutzer erfolgreich gelöscht"} json.NewEncoder(w).Encode(response) // Audit-Log: User gelöscht requestUserID, username := getUserFromRequest(r) ipAddress, userAgent := getRequestInfo(r) auditService.Track(r.Context(), "DELETE", "user", vars["id"], requestUserID, username, map[string]interface{}{ "message": fmt.Sprintf("User gelöscht: %s", vars["id"]), }, ipAddress, userAgent) } // Profilbild-Upload Handler func uploadAvatarHandler(w http.ResponseWriter, r *http.Request) { 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 } // Prüfe ob Benutzer authentifiziert ist userID, username := getUserFromRequest(r) if userID == "" { http.Error(w, "Nicht authentifiziert", http.StatusUnauthorized) return } vars := mux.Vars(r) requestedUserID := vars["id"] // Prüfe ob Benutzer sein eigenes Profilbild hochlädt if userID != requestedUserID { http.Error(w, "Sie können nur Ihr eigenes Profilbild ändern", http.StatusForbidden) return } // Parse multipart form (max 10MB) err := r.ParseMultipartForm(10 << 20) // 10MB if err != nil { http.Error(w, "Fehler beim Parsen des Formulars", http.StatusBadRequest) return } file, handler, err := r.FormFile("avatar") if err != nil { http.Error(w, "Keine Datei gefunden", http.StatusBadRequest) return } defer file.Close() // Validiere Dateityp (nur Bilder) allowedTypes := []string{"image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"} fileType := handler.Header.Get("Content-Type") isAllowed := false for _, allowedType := range allowedTypes { if fileType == allowedType { isAllowed = true break } } if !isAllowed { http.Error(w, "Nur Bilddateien (JPEG, PNG, GIF, WebP) sind erlaubt", http.StatusBadRequest) return } // Bestimme Dateiendung basierend auf Content-Type var ext string switch fileType { case "image/jpeg", "image/jpg": ext = ".jpg" case "image/png": ext = ".png" case "image/gif": ext = ".gif" case "image/webp": ext = ".webp" default: ext = ".jpg" } // Erstelle Dateiname basierend auf User-ID avatarDir := "uploads/avatars" filename := userID + ext avatarPath := filepath.Join(avatarDir, filename) // Lösche alle vorhandenen Avatar-Dateien für diesen Benutzer extensions := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} for _, oldExt := range extensions { oldPath := filepath.Join(avatarDir, userID+oldExt) if _, err := os.Stat(oldPath); err == nil { // Datei existiert, lösche sie if err := os.Remove(oldPath); err != nil { log.Printf("Warnung: Konnte alte Avatar-Datei nicht löschen: %v", err) // Weiter machen, auch wenn Löschen fehlschlägt } else { log.Printf("Alte Avatar-Datei gelöscht: %s", oldPath) } } } // Erstelle Datei dst, err := os.Create(avatarPath) if err != nil { http.Error(w, "Fehler beim Erstellen der Datei", http.StatusInternalServerError) log.Printf("Fehler beim Erstellen der Avatar-Datei: %v", err) return } defer dst.Close() // Kopiere Dateiinhalt _, err = io.Copy(dst, file) if err != nil { http.Error(w, "Fehler beim Speichern der Datei", http.StatusInternalServerError) log.Printf("Fehler beim Speichern der Avatar-Datei: %v", err) return } // Audit-Log ipAddress, userAgent := getRequestInfo(r) auditService.Track(r.Context(), "UPDATE", "user", userID, userID, username, map[string]interface{}{ "action": "avatar_uploaded", "filename": filename, }, ipAddress, userAgent) // Erfolgreiche Antwort w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Profilbild erfolgreich hochgeladen", "filename": filename, "url": fmt.Sprintf("/api/users/%s/avatar", userID), }) } // Profilbild-Abruf Handler func getAvatarHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } vars := mux.Vars(r) userID := vars["id"] // Suche nach Avatar-Datei (versuche verschiedene Formate) avatarDir := "uploads/avatars" extensions := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"} var avatarPath string var found bool for _, ext := range extensions { path := filepath.Join(avatarDir, userID+ext) if _, err := os.Stat(path); err == nil { avatarPath = path found = true break } } if !found { http.Error(w, "Profilbild nicht gefunden", http.StatusNotFound) return } // Öffne Datei file, err := os.Open(avatarPath) if err != nil { http.Error(w, "Fehler beim Öffnen der Datei", http.StatusInternalServerError) return } defer file.Close() // Bestimme Content-Type basierend auf Dateiendung ext := filepath.Ext(avatarPath) var contentType string switch ext { case ".jpg", ".jpeg": contentType = "image/jpeg" case ".png": contentType = "image/png" case ".gif": contentType = "image/gif" case ".webp": contentType = "image/webp" default: contentType = "image/jpeg" } w.Header().Set("Content-Type", contentType) w.Header().Set("Cache-Control", "public, max-age=3600") 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") } hasUpper := false hasLower := false hasDigit := false hasSpecial := false for _, char := range password { switch { case 'A' <= char && char <= 'Z': hasUpper = true case 'a' <= char && char <= 'z': hasLower = true case '0' <= char && char <= '9': hasDigit = true default: // Prüfe auf Sonderzeichen (alles was nicht Buchstabe oder Zahl ist) if !(('A' <= char && char <= 'Z') || ('a' <= char && char <= 'z') || ('0' <= char && char <= '9')) { hasSpecial = true } } } var missing []string if !hasUpper { missing = append(missing, "Großbuchstaben") } if !hasLower { missing = append(missing, "Kleinbuchstaben") } if !hasDigit { missing = append(missing, "Zahlen") } if !hasSpecial { missing = append(missing, "Sonderzeichen") } if len(missing) > 0 { return fmt.Errorf("passwort muss enthalten: %s", strings.Join(missing, ", ")) } return nil } // Helper-Funktion zum Extrahieren des Benutzers aus dem Request (für Basic Auth) func getUserFromRequest(r *http.Request) (userID, username string) { auth := r.Header.Get("Authorization") if auth == "" || !strings.HasPrefix(auth, "Basic ") { return "", "" } encoded := strings.TrimPrefix(auth, "Basic ") decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return "", "" } parts := strings.SplitN(string(decoded), ":", 2) if len(parts) != 2 { return "", "" } username = parts[0] // Hole User-ID aus der Datenbank ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() var id string var enabled int err = db.QueryRowContext(ctx, "SELECT id, enabled FROM users WHERE username = ?", username).Scan(&id, &enabled) if err != nil { // Logge Fehler nur wenn es nicht "no rows" ist if err != sql.ErrNoRows { log.Printf("Fehler beim Abrufen der User-ID für %s: %v", username, err) } return "", username } // Prüfe ob User aktiviert ist if enabled == 0 { log.Printf("API-Zugriff für deaktivierten Benutzer: %s", username) return "", username } return id, username } // UserPermissionInfo enthält die Berechtigungsinformationen eines Benutzers type UserPermissionInfo struct { UserID string Groups []PermissionGroupInfo HasFullAccess bool // true wenn der Benutzer mindestens eine FULL_ACCESS Gruppe hat } // PermissionGroupInfo enthält Informationen über eine Berechtigungsgruppe type PermissionGroupInfo struct { GroupID string Permission PermissionLevel SpaceIDs []string // Leer bedeutet Zugriff auf alle Spaces } // isUserAdmin prüft, ob ein Benutzer Admin ist func isUserAdmin(userID string) (bool, error) { if userID == "" { return false, nil } ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() var isAdmin int err := db.QueryRowContext(ctx, "SELECT is_admin FROM users WHERE id = ?", userID).Scan(&isAdmin) if err != nil { if err == sql.ErrNoRows { return false, nil } return false, err } return isAdmin == 1, nil } // getUserPermissions ruft die Berechtigungen eines Benutzers ab func getUserPermissions(userID string) (*UserPermissionInfo, error) { if userID == "" { return nil, fmt.Errorf("userID ist leer") } ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() // Prüfe ob User Admin ist - Admins haben immer Vollzugriff isAdmin, err := isUserAdmin(userID) if err != nil { log.Printf("Fehler beim Prüfen des Admin-Status: %v", err) } if isAdmin { return &UserPermissionInfo{ UserID: userID, Groups: []PermissionGroupInfo{}, HasFullAccess: true, }, nil } // Hole alle Gruppen des Benutzers mit ihren Berechtigungen query := ` SELECT pg.id, pg.permission FROM permission_groups pg INNER JOIN user_groups ug ON pg.id = ug.group_id WHERE ug.user_id = ? ` rows, err := db.QueryContext(ctx, query, userID) if err != nil { return nil, fmt.Errorf("fehler beim Abrufen der Benutzergruppen: %w", err) } defer rows.Close() info := &UserPermissionInfo{ UserID: userID, Groups: []PermissionGroupInfo{}, } var groupIDs []string for rows.Next() { var groupID string var permission string if err := rows.Scan(&groupID, &permission); err != nil { continue } groupIDs = append(groupIDs, groupID) groupInfo := PermissionGroupInfo{ GroupID: groupID, Permission: PermissionLevel(permission), SpaceIDs: []string{}, } if PermissionLevel(permission) == PermissionFullAccess { info.HasFullAccess = true } info.Groups = append(info.Groups, groupInfo) } // Hole Space-Zuweisungen für alle Gruppen if len(groupIDs) > 0 { placeholders := make([]string, len(groupIDs)) args := make([]interface{}, len(groupIDs)) for i, id := range groupIDs { placeholders[i] = "?" args[i] = id } spaceQuery := fmt.Sprintf(` SELECT group_id, space_id FROM group_spaces WHERE group_id IN (%s) `, strings.Join(placeholders, ",")) spaceRows, err := db.QueryContext(ctx, spaceQuery, args...) if err == nil { spaceMap := make(map[string][]string) for spaceRows.Next() { var groupID, spaceID string if err := spaceRows.Scan(&groupID, &spaceID); err == nil { spaceMap[groupID] = append(spaceMap[groupID], spaceID) } } spaceRows.Close() // Aktualisiere SpaceIDs für jede Gruppe for i := range info.Groups { if spaceIDs, exists := spaceMap[info.Groups[i].GroupID]; exists { info.Groups[i].SpaceIDs = spaceIDs } } } } return info, nil } // hasSpaceAccess prüft, ob ein Benutzer Zugriff auf einen bestimmten Space hat func hasSpaceAccess(userID, spaceID string) (bool, error) { if userID == "" { return false, nil } // Admins haben immer Zugriff isAdmin, err := isUserAdmin(userID) if err == nil && isAdmin { return true, nil } permissions, err := getUserPermissions(userID) if err != nil { return false, err } // Wenn der Benutzer keine Gruppen hat und nicht Admin ist, hat er keinen Zugriff // Admins haben immer Zugriff (wird bereits oben geprüft) if !isAdmin && len(permissions.Groups) == 0 { return false, nil } // Prüfe für jede Gruppe, ob der Benutzer Zugriff auf den Space hat for _, group := range permissions.Groups { // Wenn die Gruppe keine Spaces zugewiesen hat, hat der Benutzer Zugriff auf alle Spaces if len(group.SpaceIDs) == 0 { return true, nil } // Prüfe, ob der Space in der Liste der zugewiesenen Spaces ist for _, assignedSpaceID := range group.SpaceIDs { if assignedSpaceID == spaceID { return true, nil } } } return false, nil } // hasPermission prüft, ob ein Benutzer eine bestimmte Berechtigung für einen Space hat // requiredPermission kann READ, READ_WRITE oder FULL_ACCESS sein func hasPermission(userID, spaceID string, requiredPermission PermissionLevel) (bool, error) { if userID == "" { return false, nil } // Admins haben immer alle Berechtigungen isAdmin, err := isUserAdmin(userID) if err == nil && isAdmin { return true, nil } permissions, err := getUserPermissions(userID) if err != nil { return false, err } // Wenn der Benutzer keine Gruppen hat und nicht Admin ist, hat er keine Berechtigung // Admins haben immer alle Berechtigungen (wird bereits oben geprüft) if !isAdmin && len(permissions.Groups) == 0 { return false, nil } // Prüfe für jede Gruppe for _, group := range permissions.Groups { hasAccess := false // Prüfe, ob der Benutzer Zugriff auf den Space hat if len(group.SpaceIDs) == 0 { // Keine Space-Zuweisungen = Zugriff auf alle Spaces hasAccess = true } else { // Prüfe, ob der Space in der Liste ist for _, assignedSpaceID := range group.SpaceIDs { if assignedSpaceID == spaceID { hasAccess = true break } } } if !hasAccess { continue } // Prüfe die Berechtigungsstufe switch requiredPermission { case PermissionRead: // READ, READ_WRITE und FULL_ACCESS haben alle READ-Berechtigung return true, nil case PermissionReadWrite: // READ_WRITE und FULL_ACCESS haben READ_WRITE-Berechtigung if group.Permission == PermissionReadWrite || group.Permission == PermissionFullAccess { return true, nil } case PermissionFullAccess: // Nur FULL_ACCESS hat FULL_ACCESS-Berechtigung if group.Permission == PermissionFullAccess { return true, nil } } } return false, nil } // getAccessibleSpaceIDs gibt alle Space-IDs zurück, auf die der Benutzer Zugriff hat func getAccessibleSpaceIDs(userID string) ([]string, error) { if userID == "" { return []string{}, nil } // Prüfe ob User Admin ist - Admins haben Zugriff auf alle Spaces isAdmin, err := isUserAdmin(userID) if err == nil && isAdmin { // Hole alle Spaces für Admin ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() rows, err := db.QueryContext(ctx, "SELECT id FROM spaces") if err != nil { return []string{}, err } defer rows.Close() var spaceIDs []string for rows.Next() { var spaceID string if err := rows.Scan(&spaceID); err == nil { spaceIDs = append(spaceIDs, spaceID) } } return spaceIDs, nil } permissions, err := getUserPermissions(userID) if err != nil { return []string{}, err } // Wenn der Benutzer keine Gruppen hat, hat er keinen Zugriff // (Admin wurde bereits oben behandelt) if len(permissions.Groups) == 0 { return []string{}, nil } // Sammle alle zugewiesenen Spaces spaceIDMap := make(map[string]bool) hasUnrestrictedAccess := false for _, group := range permissions.Groups { // Wenn eine Gruppe keine Spaces zugewiesen hat, hat der Benutzer Zugriff auf alle Spaces if len(group.SpaceIDs) == 0 { hasUnrestrictedAccess = true break } // Sammle alle zugewiesenen Spaces for _, spaceID := range group.SpaceIDs { spaceIDMap[spaceID] = true } } // Wenn der Benutzer Zugriff auf alle Spaces hat, hole alle Spaces aus der DB if hasUnrestrictedAccess { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() rows, err := db.QueryContext(ctx, "SELECT id FROM spaces") if err != nil { return []string{}, err } defer rows.Close() var spaceIDs []string for rows.Next() { var spaceID string if err := rows.Scan(&spaceID); err == nil { spaceIDs = append(spaceIDs, spaceID) } } return spaceIDs, nil } // Konvertiere Map zu Slice spaceIDs := make([]string, 0, len(spaceIDMap)) for spaceID := range spaceIDMap { spaceIDs = append(spaceIDs, spaceID) } return spaceIDs, nil } // Helper-Funktion zum Extrahieren von IP-Adresse und User-Agent aus Request func getRequestInfo(r *http.Request) (ipAddress, userAgent string) { // Hole IP-Adresse ipAddress = r.RemoteAddr if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" { ipAddress = forwarded } else if realIP := r.Header.Get("X-Real-IP"); realIP != "" { ipAddress = realIP } // 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 } // Audit Log Handler func getAuditLogsHandler(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 } // Query-Parameter für Filterung und Pagination query := r.URL.Query() limitStr := query.Get("limit") offsetStr := query.Get("offset") actionFilter := query.Get("action") resourceTypeFilter := query.Get("resourceType") userIdFilter := query.Get("userId") // Standardwerte für Pagination limit := 100 offset := 0 if limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 1000 { limit = l } } if offsetStr != "" { if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 { offset = o } } // Baue SQL-Query mit Filtern whereClauses := []string{} args := []interface{}{} if actionFilter != "" { whereClauses = append(whereClauses, "action = ?") args = append(args, actionFilter) } if resourceTypeFilter != "" { whereClauses = append(whereClauses, "resource_type = ?") args = append(args, resourceTypeFilter) } if userIdFilter != "" { whereClauses = append(whereClauses, "user_id = ?") args = append(args, userIdFilter) } whereSQL := "" if len(whereClauses) > 0 { whereSQL = "WHERE " + strings.Join(whereClauses, " AND ") } // Zähle Gesamtanzahl für Pagination var totalCount int countSQL := "SELECT COUNT(*) FROM audit_logs " + whereSQL countCtx, countCancel := context.WithTimeout(context.Background(), time.Second*5) defer countCancel() err := db.QueryRowContext(countCtx, countSQL, args...).Scan(&totalCount) if err != nil { http.Error(w, "Fehler beim Zählen der Logs", http.StatusInternalServerError) log.Printf("Fehler beim Zählen der Logs: %v", err) return } // 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 FROM audit_logs %s ORDER BY datetime(timestamp) DESC, id DESC LIMIT ? OFFSET ? `, whereSQL) queryArgs = make([]interface{}, len(args)) copy(queryArgs, args) queryArgs = append(queryArgs, limit, offset) } else { querySQL = ` SELECT id, timestamp, user_id, username, action, resource_type, resource_id, details, ip_address, user_agent FROM audit_logs ORDER BY datetime(timestamp) DESC, id DESC LIMIT ? OFFSET ? ` queryArgs = []interface{}{limit, offset} } 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) log.Printf("Fehler beim Abrufen der Logs: %v", err) return } defer rows.Close() log.Printf("SQL Query: %s, Args: %v (Count: %d)", querySQL, queryArgs, len(queryArgs)) var logs []AuditLog rowCount := 0 scanErrors := 0 for rows.Next() { rowCount++ var logEntry AuditLog var userID, username, resourceID, detailsJSON, ipAddress, userAgent sql.NullString err := rows.Scan( &logEntry.ID, &logEntry.Timestamp, &userID, &username, &logEntry.Action, &logEntry.ResourceType, &resourceID, &detailsJSON, &ipAddress, &userAgent, ) if err != nil { scanErrors++ log.Printf("Fehler beim Scannen der Log-Zeile %d: %v", rowCount, err) continue } if userID.Valid { logEntry.UserID = userID.String } if username.Valid { logEntry.Username = username.String } if resourceID.Valid { logEntry.ResourceID = resourceID.String } // Parse JSON details if detailsJSON.Valid && detailsJSON.String != "" { var detailsMap map[string]interface{} if err := json.Unmarshal([]byte(detailsJSON.String), &detailsMap); err == nil { // Convert map to JSON string for display if jsonBytes, err := json.Marshal(detailsMap); err == nil { logEntry.Details = string(jsonBytes) } else { logEntry.Details = detailsJSON.String } } else { // Fallback: use raw string if JSON parsing fails logEntry.Details = detailsJSON.String } } if ipAddress.Valid { logEntry.IPAddress = ipAddress.String } if userAgent.Valid { logEntry.UserAgent = userAgent.String } logs = append(logs, logEntry) } if err := rows.Err(); err != nil { log.Printf("Fehler beim Iterieren über die Zeilen: %v", err) } 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, } log.Printf("Audit-Logs abgerufen: %d Einträge gefunden (Total: %d, Limit: %d, Offset: %d)", len(logs), totalCount, limit, offset) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } // Handler zum Löschen aller Audit-Logs func deleteAllAuditLogsHandler(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 } // Prüfe Bestätigung confirm := r.URL.Query().Get("confirm") if confirm != "true" { http.Error(w, "Bestätigung erforderlich. Verwende ?confirm=true", http.StatusBadRequest) return } ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() // Lösche alle Audit-Logs result, err := db.ExecContext(ctx, "DELETE FROM audit_logs") if err != nil { http.Error(w, "Fehler beim Löschen der Audit-Logs", http.StatusInternalServerError) log.Printf("Fehler beim Löschen der Audit-Logs: %v", err) return } rowsAffected, err := result.RowsAffected() if err != nil { http.Error(w, "Fehler beim Prüfen der gelöschten Zeilen", http.StatusInternalServerError) log.Printf("Fehler beim Prüfen der gelöschten Zeilen: %v", err) return } log.Printf("Alle Audit-Logs gelöscht: %d Einträge", rowsAffected) response := map[string]interface{}{ "message": "Alle Audit-Logs erfolgreich gelöscht", "deletedCount": rowsAffected, } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } // Handler zum Erstellen eines Test-Audit-Logs (für Testskripte) func createTestAuditLogHandler(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 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"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Ungültige Anfrage", http.StatusBadRequest) return } if req.Action == "" || req.Entity == "" { http.Error(w, "action und entity sind erforderlich", http.StatusBadRequest) return } // Verwende AuditService zum Erstellen des Logs if auditService == nil { http.Error(w, "AuditService nicht initialisiert", http.StatusInternalServerError) return } // 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) response := map[string]interface{}{ "success": true, "message": "Test-Audit-Log erstellt", } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } // Basic Auth Middleware func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // OPTIONS-Requests erlauben (für CORS) if r.Method == "OPTIONS" { next(w, r) return } // Prüfe ob es ein AJAX/Fetch-Request ist (kein Browser-Basic-Auth-Dialog) isAjaxRequest := r.Header.Get("X-Requested-With") == "XMLHttpRequest" || strings.Contains(r.Header.Get("Content-Type"), "application/json") || r.Header.Get("Accept") == "application/json" || strings.HasPrefix(r.URL.Path, "/api/") // Prüfe Authorization Header auth := r.Header.Get("Authorization") if auth == "" { if !isAjaxRequest { w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "Authentifizierung erforderlich"}) return } // Parse Basic Auth if !strings.HasPrefix(auth, "Basic ") { if !isAjaxRequest { w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"}) return } // Decode Base64 encoded := strings.TrimPrefix(auth, "Basic ") decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { if !isAjaxRequest { w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"}) return } // Split username:password parts := strings.SplitN(string(decoded), ":", 2) if len(parts) != 2 { if !isAjaxRequest { w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"}) return } username := parts[0] password := parts[1] // Validiere Benutzer ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() var storedHash string 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 { w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Anmeldedaten"}) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{"error": "Fehler bei der Authentifizierung"}) log.Printf("Fehler bei der Authentifizierung: %v", err) 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 { if !isAjaxRequest { w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Anmeldedaten"}) return } // Authentifizierung erfolgreich - weiterleiten next(w, r) } } // 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") 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, Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } // Prüfe Authorization Header auth := r.Header.Get("Authorization") if auth == "" { w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "Authentifizierung erforderlich"}) return } // Parse Basic Auth if !strings.HasPrefix(auth, "Basic ") { w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"}) return } // Decode Base64 encoded := strings.TrimPrefix(auth, "Basic ") decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { log.Printf("Fehler beim Decodieren der Basic Auth: %v", err) w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"}) return } // Split username:password parts := strings.SplitN(string(decoded), ":", 2) if len(parts) != 2 { log.Printf("Ungültiges Format in Basic Auth: %s", string(decoded)) w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Authentifizierung"}) return } username := parts[0] password := parts[1] log.Printf("Login-Versuch für Benutzer: %s", username) // Validiere Benutzer ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() var user User var storedHash string 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) w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Anmeldedaten"}) return } log.Printf("Fehler beim Abrufen des Benutzers: %v", err) w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(map[string]string{"error": "Fehler bei der Authentifizierung"}) 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 { log.Printf("Passwort-Validierung fehlgeschlagen für Benutzer: %s", username) w.WriteHeader(http.StatusUnauthorized) json.NewEncoder(w).Encode(map[string]string{"error": "Ungültige Anmeldedaten"}) return } log.Printf("Login erfolgreich für Benutzer: %s", username) // Login erfolgreich response := map[string]interface{}{ "success": true, "user": user, "message": "Login erfolgreich", } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } func main() { log.Println("Starte certigo-addon Backend...") // Initialisiere Datenbank log.Println("Initialisiere Datenbank...") initDB() defer func() { log.Println("Schließe Datenbankverbindung...") db.Close() }() log.Println("Datenbank initialisiert") // Initialisiere Provider pm := providers.GetManager() pm.RegisterProvider(providers.NewDummyCAProvider()) pm.RegisterProvider(providers.NewAutoDNSProvider()) pm.RegisterProvider(providers.NewHetznerProvider()) r := mux.NewRouter() // Swagger UI Route r.HandleFunc("/swagger", swaggerUIHandler).Methods("GET") r.HandleFunc("/api/openapi.yaml", openAPIHandler).Methods("GET") // 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", 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 (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") api.HandleFunc("/providers/{id}", basicAuthMiddleware(getProviderHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/providers/{id}/enabled", basicAuthMiddleware(setProviderEnabledHandler)).Methods("PUT", "OPTIONS") api.HandleFunc("/providers/{id}/config", basicAuthMiddleware(updateProviderConfigHandler)).Methods("PUT", "OPTIONS") api.HandleFunc("/providers/{id}/test", basicAuthMiddleware(testProviderConnectionHandler)).Methods("POST", "OPTIONS") api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr/sign", basicAuthMiddleware(signCSRHandler)).Methods("POST", "OPTIONS") api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/certificates", basicAuthMiddleware(getCertificatesHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/certificates/{certId}/refresh", basicAuthMiddleware(refreshCertificateHandler)).Methods("POST", "OPTIONS") // Audit Log Routes api.HandleFunc("/audit-logs", basicAuthMiddleware(getAuditLogsHandler)).Methods("GET", "OPTIONS") api.HandleFunc("/audit-logs", basicAuthMiddleware(deleteAllAuditLogsHandler)).Methods("DELETE", "OPTIONS") api.HandleFunc("/audit-logs/test", basicAuthMiddleware(createTestAuditLogHandler)).Methods("POST", "OPTIONS") // Start server port := ":8080" log.Printf("Server läuft auf Port %s", port) log.Fatal(http.ListenAndServe(port, r)) } // Provider Handlers func getProvidersHandler(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 } pm := providers.GetManager() allProviders := pm.GetAllProviders() // Definiere feste Reihenfolge der Provider providerOrder := []string{"dummy-ca", "autodns", "hetzner"} // Erstelle Map für schnellen Zugriff providerMap := make(map[string]providers.ProviderInfo) for id, provider := range allProviders { config, _ := pm.GetProviderConfig(id) providerInfo := providers.ProviderInfo{ ID: id, Name: provider.GetName(), DisplayName: provider.GetDisplayName(), Description: provider.GetDescription(), Enabled: config.Enabled, Settings: provider.GetRequiredSettings(), } providerMap[id] = providerInfo } // Sortiere nach definierter Reihenfolge var providerInfos []providers.ProviderInfo for _, id := range providerOrder { if providerInfo, exists := providerMap[id]; exists { providerInfos = append(providerInfos, providerInfo) delete(providerMap, id) } } // Füge alle anderen Provider hinzu, die nicht in der Liste sind for _, providerInfo := range providerMap { providerInfos = append(providerInfos, providerInfo) } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(providerInfos) } func getProviderHandler(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) id := vars["id"] pm := providers.GetManager() provider, exists := pm.GetProvider(id) if !exists { http.Error(w, "Provider nicht gefunden", http.StatusNotFound) return } config, _ := pm.GetProviderConfig(id) providerInfo := providers.ProviderInfo{ ID: id, Name: provider.GetName(), DisplayName: provider.GetDisplayName(), Description: provider.GetDescription(), Enabled: config.Enabled, Settings: provider.GetRequiredSettings(), } // Füge aktuelle Konfigurationswerte hinzu (ohne Passwörter) safeSettings := make(map[string]interface{}) for key, value := range config.Settings { // Verstecke Passwörter und API Keys in der Antwort if key == "password" || key == "apiKey" { if str, ok := value.(string); ok && str != "" { safeSettings[key] = "***" } else { safeSettings[key] = value } } else { safeSettings[key] = value } } // Konvertiere zu JSON für die Response safeSettingsJSON, _ := json.Marshal(safeSettings) var safeSettingsMap map[string]interface{} json.Unmarshal(safeSettingsJSON, &safeSettingsMap) response := map[string]interface{}{ "id": providerInfo.ID, "name": providerInfo.Name, "displayName": providerInfo.DisplayName, "description": providerInfo.Description, "enabled": providerInfo.Enabled, "settings": providerInfo.Settings, "config": safeSettingsMap, } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } func setProviderEnabledHandler(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) id := vars["id"] var req struct { Enabled bool `json:"enabled"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } pm := providers.GetManager() if err := pm.SetProviderEnabled(id, req.Enabled); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Provider-Status erfolgreich aktualisiert", "enabled": req.Enabled, }) // Audit-Log: Provider aktiviert/deaktiviert userID, username := getUserFromRequest(r) ipAddress, userAgent := getRequestInfo(r) action := "DISABLE" if req.Enabled { action = "ENABLE" } auditService.Track(r.Context(), action, "provider", id, userID, username, map[string]interface{}{ "enabled": req.Enabled, "message": fmt.Sprintf("Provider %s %s", id, strings.ToLower(action)), }, ipAddress, userAgent) } func updateProviderConfigHandler(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) id := vars["id"] var req struct { Settings map[string]interface{} `json:"settings"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } pm := providers.GetManager() config, _ := pm.GetProviderConfig(id) config.Settings = req.Settings if err := pm.UpdateProviderConfig(id, config); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "message": "Konfiguration erfolgreich aktualisiert", }) // Audit-Log: Provider-Konfiguration aktualisiert userID, username := getUserFromRequest(r) ipAddress, userAgent := getRequestInfo(r) auditService.Track(r.Context(), "UPDATE", "provider", id, userID, username, map[string]interface{}{ "message": fmt.Sprintf("Provider-Konfiguration aktualisiert: %s", id), }, ipAddress, userAgent) } func testProviderConnectionHandler(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 } vars := mux.Vars(r) id := vars["id"] var req struct { Settings map[string]interface{} `json:"settings"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } pm := providers.GetManager() provider, exists := pm.GetProvider(id) if !exists { http.Error(w, "Provider nicht gefunden", http.StatusNotFound) return } if err := provider.TestConnection(req.Settings); err != nil { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "success": false, "message": err.Error(), }) return } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": "Verbindung erfolgreich", }) } func signCSRHandler(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 } vars := mux.Vars(r) 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 } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } if req.ProviderID == "" { http.Error(w, "providerId ist erforderlich", http.StatusBadRequest) return } // Hole neuesten CSR für den FQDN var csrPEM string var csrID string err = db.QueryRow(` SELECT id, csr_pem FROM csrs WHERE fqdn_id = ? AND space_id = ? ORDER BY created_at DESC LIMIT 1 `, fqdnID, spaceID).Scan(&csrID, &csrPEM) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Kein CSR für diesen FQDN gefunden", http.StatusNotFound) return } http.Error(w, "Fehler beim Laden des CSR", http.StatusInternalServerError) log.Printf("Fehler beim Laden des CSR: %v", err) return } // Wenn spezifischer CSR angefordert wurde if req.CSRID != "" && req.CSRID != csrID { err := db.QueryRow(` SELECT csr_pem FROM csrs WHERE id = ? AND fqdn_id = ? AND space_id = ? `, req.CSRID, fqdnID, spaceID).Scan(&csrPEM) if err != nil { if err == sql.ErrNoRows { http.Error(w, "CSR nicht gefunden", http.StatusNotFound) return } http.Error(w, "Fehler beim Laden des CSR", http.StatusInternalServerError) return } csrID = req.CSRID } // Hole Provider pm := providers.GetManager() provider, exists := pm.GetProvider(req.ProviderID) if !exists { http.Error(w, "Provider nicht gefunden", http.StatusNotFound) return } // Prüfe ob Provider aktiviert ist config, err := pm.GetProviderConfig(req.ProviderID) if err != nil || !config.Enabled { http.Error(w, "Provider ist nicht aktiviert", http.StatusBadRequest) return } // Signiere CSR result, err := provider.SignCSR(csrPEM, config.Settings) if err != nil { http.Error(w, fmt.Sprintf("Fehler beim Signieren des CSR: %v", err), http.StatusInternalServerError) log.Printf("Fehler beim Signieren des CSR: %v", err) return } // Speichere das Zertifikat in der DB certID := uuid.New().String() createdAt := time.Now() _, err = db.Exec(` INSERT INTO certificates (id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, certID, fqdnID, spaceID, csrID, result.OrderID, req.ProviderID, result.CertificatePEM, result.Status, createdAt) if err != nil { log.Printf("Fehler beim Speichern des Zertifikats: %v", err) // Weiterhin erfolgreich zurückgeben, auch wenn Speichern fehlschlägt } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "message": result.Message, "certificateId": certID, "orderId": result.OrderID, "status": result.Status, "csrId": csrID, }) // Audit-Log: CSR signiert ipAddress, userAgent := getRequestInfo(r) auditService.Track(r.Context(), "SIGN", "csr", csrID, userID, username, map[string]interface{}{ "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), }, ipAddress, userAgent) } func getCertificatesHandler(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) 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 FROM certificates WHERE fqdn_id = ? AND space_id = ? ORDER BY created_at DESC `, fqdnID, spaceID) if err != nil { http.Error(w, "Fehler beim Laden der Zertifikate", http.StatusInternalServerError) log.Printf("Fehler beim Laden der Zertifikate: %v", err) return } defer rows.Close() var certificates []map[string]interface{} for rows.Next() { var id, csrID, certID, providerID, certPEM, status, createdAt string err := rows.Scan(&id, &csrID, &certID, &providerID, &certPEM, &status, &createdAt) if err != nil { log.Printf("Fehler beim Scannen der Zertifikat-Zeile: %v", err) continue } certificates = append(certificates, map[string]interface{}{ "id": id, "csrId": csrID, "certificateId": certID, "providerId": providerID, "certificatePEM": certPEM, "status": status, "createdAt": createdAt, }) } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(certificates) } func refreshCertificateHandler(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 } vars := mux.Vars(r) spaceID := vars["spaceId"] 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(` SELECT certificate_id, provider_id FROM certificates WHERE id = ? AND fqdn_id = ? AND space_id = ? `, certID, fqdnID, spaceID).Scan(&certificateID, &providerID) if err != nil { if err == sql.ErrNoRows { http.Error(w, "Zertifikat nicht gefunden", http.StatusNotFound) return } http.Error(w, "Fehler beim Laden des Zertifikats", http.StatusInternalServerError) return } // Hole Provider pm := providers.GetManager() provider, exists := pm.GetProvider(providerID) if !exists { http.Error(w, "Provider nicht gefunden", http.StatusNotFound) return } // Prüfe ob Provider aktiviert ist config, err := pm.GetProviderConfig(providerID) if err != nil || !config.Enabled { http.Error(w, "Provider ist nicht aktiviert", http.StatusBadRequest) return } // Rufe Zertifikat von CA ab certPEM, err := provider.GetCertificate(certificateID, config.Settings) if err != nil { http.Error(w, fmt.Sprintf("Fehler beim Abrufen des Zertifikats: %v", err), http.StatusInternalServerError) return } // Aktualisiere Zertifikat in DB _, err = db.Exec(` UPDATE certificates SET certificate_pem = ? WHERE id = ? AND fqdn_id = ? AND space_id = ? `, certPEM, certID, fqdnID, spaceID) if err != nil { log.Printf("Fehler beim Aktualisieren des Zertifikats: %v", err) } w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "certificatePEM": certPEM, "certificateId": certificateID, }) }