diff --git a/backend/main.go b/backend/main.go
index 41b4c48..ab497b4 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -261,6 +261,8 @@ 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"`
}
@@ -306,6 +308,8 @@ type UpdateUserRequest struct {
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"`
}
@@ -314,6 +318,7 @@ 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"`
}
@@ -535,6 +540,8 @@ func initDB() {
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
+ is_admin INTEGER DEFAULT 0,
+ enabled INTEGER DEFAULT 1,
created_at DATETIME NOT NULL
);`
@@ -550,6 +557,22 @@ func initDB() {
log.Println("Datenbank erfolgreich initialisiert")
+ // Füge is_admin Spalte hinzu falls nicht vorhanden
+ ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
+ _, err = db.ExecContext(ctx, "ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0")
+ cancel()
+ if err != nil && !strings.Contains(err.Error(), "duplicate column") {
+ log.Printf("Hinweis: is_admin-Spalte könnte bereits existieren: %v", err)
+ }
+
+ // Füge enabled Spalte hinzu falls nicht vorhanden
+ ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
+ _, err = db.ExecContext(ctx, "ALTER TABLE users ADD COLUMN enabled INTEGER DEFAULT 1")
+ cancel()
+ if err != nil && !strings.Contains(err.Error(), "duplicate column") {
+ log.Printf("Hinweis: enabled-Spalte könnte bereits existieren: %v", err)
+ }
+
// Erstelle Audit-Log-Tabelle
log.Println("Erstelle audit_logs-Tabelle...")
createAuditLogsTableSQL := `
@@ -679,19 +702,26 @@ func createDefaultAdmin() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
- // Prüfe ob bereits ein Admin-User existiert
+ // Prüfe ob bereits ein Admin-User mit UID "admin" existiert
var count int
- err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = 'admin'").Scan(&count)
+ err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE id = 'admin'").Scan(&count)
if err != nil {
log.Printf("Fehler beim Prüfen des Admin-Users: %v", err)
return
}
if count > 0 {
- log.Println("Admin-User existiert bereits")
+ log.Println("Admin-User mit UID 'admin' existiert bereits")
+ // Stelle sicher, dass der Admin-User als Admin markiert ist
+ _, err = db.ExecContext(ctx, "UPDATE users SET is_admin = 1 WHERE id = 'admin'")
+ if err != nil {
+ log.Printf("Warnung: Konnte Admin-Status nicht setzen: %v", err)
+ } else {
+ log.Println("Admin-User ist als Administrator markiert")
+ }
// Prüfe ob das Passwort noch "admin" ist (für Debugging)
var storedHash string
- err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE username = 'admin'").Scan(&storedHash)
+ err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE id = 'admin'").Scan(&storedHash)
if err == nil {
// Teste ob das Passwort "admin" ist
testErr := bcrypt.CompareHashAndPassword([]byte(storedHash), []byte("admin"))
@@ -704,7 +734,45 @@ func createDefaultAdmin() {
return
}
- // Erstelle Default Admin-User
+ // Migration: Falls ein Admin-User mit username='admin' aber anderer UID existiert, migriere ihn
+ var existingAdminID string
+ err = db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = 'admin' AND id != 'admin' LIMIT 1").Scan(&existingAdminID)
+ if err == nil {
+ log.Printf("Migriere bestehenden Admin-User von UID '%s' zu UID 'admin'", existingAdminID)
+ // Hole alle Daten des alten Admin-Users
+ var oldUsername, oldEmail, oldPasswordHash string
+ var oldIsAdmin, oldEnabled int
+ var oldCreatedAt string
+ err = db.QueryRowContext(ctx, "SELECT username, email, password_hash, is_admin, enabled, created_at FROM users WHERE id = ?", existingAdminID).
+ Scan(&oldUsername, &oldEmail, &oldPasswordHash, &oldIsAdmin, &oldEnabled, &oldCreatedAt)
+ if err == nil {
+ // Erstelle neuen Admin-User mit UID 'admin'
+ _, err = db.ExecContext(ctx,
+ "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ "admin", oldUsername, oldEmail, oldPasswordHash, oldIsAdmin, oldEnabled, oldCreatedAt)
+ if err == nil {
+ // Migriere user_groups Zuweisungen
+ _, err = db.ExecContext(ctx, "UPDATE user_groups SET user_id = 'admin' WHERE user_id = ?", existingAdminID)
+ if err != nil {
+ log.Printf("Warnung: Konnte user_groups nicht migrieren: %v", err)
+ }
+ // Lösche den alten User (CASCADE sollte user_groups automatisch löschen)
+ _, err = db.ExecContext(ctx, "DELETE FROM users WHERE id = ?", existingAdminID)
+ if err == nil {
+ log.Printf("✓ Admin-User erfolgreich zu UID 'admin' migriert")
+ return
+ } else {
+ log.Printf("Warnung: Konnte alten Admin-User nicht löschen: %v", err)
+ }
+ } else {
+ log.Printf("Warnung: Konnte neuen Admin-User nicht erstellen: %v", err)
+ }
+ } else {
+ log.Printf("Warnung: Konnte Daten des alten Admin-Users nicht lesen: %v", err)
+ }
+ }
+
+ // Erstelle Default Admin-User mit fester UID "admin"
adminPassword := "admin" // Default Passwort - sollte in Produktion geändert werden
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
if err != nil {
@@ -712,18 +780,18 @@ func createDefaultAdmin() {
return
}
- adminID := uuid.New().String()
+ adminID := "admin" // Feste UID statt UUID
createdAt := time.Now().Format(time.RFC3339)
_, err = db.ExecContext(ctx,
- "INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)",
- adminID, "admin", "admin@certigo.local", string(hashedPassword), createdAt)
+ "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ adminID, "admin", "admin@certigo.local", string(hashedPassword), 1, 1, createdAt)
if err != nil {
log.Printf("Fehler beim Erstellen des Admin-Users: %v", err)
return
}
- log.Println("✓ Default Admin-User erstellt: username='admin', password='admin'")
+ log.Println("✓ Default Admin-User erstellt: UID='admin', username='admin', password='admin'")
log.Printf(" User ID: %s", adminID)
log.Printf(" Email: admin@certigo.local")
}
@@ -2765,6 +2833,13 @@ func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) {
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 {
@@ -2781,11 +2856,12 @@ func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) {
response := map[string]interface{}{
"userId": userID,
- "hasFullAccess": permissions.HasFullAccess,
+ "isAdmin": isAdmin,
+ "hasFullAccess": permissions.HasFullAccess || isAdmin,
"accessibleSpaces": []string{},
"permissions": map[string]interface{}{
- "canCreateSpace": false,
- "canDeleteSpace": false,
+ "canCreateSpace": permissions.HasFullAccess || isAdmin,
+ "canDeleteSpace": permissions.HasFullAccess || isAdmin,
"canCreateFqdn": canCreateFqdn,
"canDeleteFqdn": canDeleteFqdn,
"canUploadCSR": canUploadCSR,
@@ -2838,17 +2914,46 @@ func getUserPermissionsHandler(w http.ResponseWriter, r *http.Request) {
}
// Prüfe globale Berechtigungen (Space erstellen/löschen)
- hasFullAccessGlobal := false
- for _, group := range permissions.Groups {
- if group.Permission == PermissionFullAccess {
- hasFullAccessGlobal = true
- break
+ // 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
+ perms := response["permissions"].(map[string]interface{})
+ perms["canCreateSpace"] = hasFullAccessGlobal
+ perms["canDeleteSpace"] = hasFullAccessGlobal
+ }
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
@@ -2869,7 +2974,7 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) {
defer cancel()
// Lade alle Benutzer
- rows, err := db.QueryContext(ctx, "SELECT id, username, email, created_at FROM users ORDER BY created_at DESC")
+ 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)
@@ -2881,12 +2986,15 @@ func getUsersHandler(w http.ResponseWriter, r *http.Request) {
var userIDs []string
for rows.Next() {
var user User
- err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
+ var isAdmin, enabled int
+ err := rows.Scan(&user.ID, &user.Username, &user.Email, &isAdmin, &enabled, &user.CreatedAt)
if err != nil {
http.Error(w, "Fehler beim Lesen der Benutzerdaten", http.StatusInternalServerError)
log.Printf("Fehler beim Lesen der Benutzerdaten: %v", err)
return
}
+ user.IsAdmin = isAdmin == 1
+ user.Enabled = enabled == 1
user.GroupIDs = []string{} // Initialisiere als leeres Array
users = append(users, user)
userIDs = append(userIDs, user.ID)
@@ -2946,8 +3054,9 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) {
defer cancel()
var user User
- err := db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID).
- Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
+ var isAdmin, enabled int
+ err := db.QueryRowContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users WHERE id = ?", userID).
+ Scan(&user.ID, &user.Username, &user.Email, &isAdmin, &enabled, &user.CreatedAt)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
@@ -2957,6 +3066,8 @@ func getUserHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Fehler beim Abrufen des Benutzers: %v", err)
return
}
+ user.IsAdmin = isAdmin == 1
+ user.Enabled = enabled == 1
// Lade Gruppen-IDs für diesen Benutzer
groupRows, err := db.QueryContext(ctx, "SELECT group_id FROM user_groups WHERE user_id = ?", userID)
@@ -3022,9 +3133,18 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
+ isAdmin := 0
+ if req.IsAdmin {
+ isAdmin = 1
+ }
+ // Admin muss immer enabled sein
+ enabledValue := 1
+ if req.IsAdmin {
+ enabledValue = 1 // Admin immer enabled
+ }
_, err = db.ExecContext(ctx,
- "INSERT INTO users (id, username, email, password_hash, created_at) VALUES (?, ?, ?, ?, ?)",
- userID, req.Username, req.Email, string(hashedPassword), createdAt)
+ "INSERT INTO users (id, username, email, password_hash, is_admin, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
+ userID, req.Username, req.Email, string(hashedPassword), isAdmin, enabledValue, createdAt)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint failed") {
if strings.Contains(err.Error(), "username") {
@@ -3058,6 +3178,8 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
ID: userID,
Username: req.Username,
Email: req.Email,
+ IsAdmin: req.IsAdmin,
+ Enabled: true,
CreatedAt: createdAt,
GroupIDs: req.GroupIDs,
}
@@ -3068,12 +3190,17 @@ func createUserHandler(w http.ResponseWriter, r *http.Request) {
// Audit-Log: User erstellt
requestUserID, requestUsername := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
- auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, map[string]interface{}{
+ auditDetails := map[string]interface{}{
"username": req.Username,
"email": req.Email,
"groupIds": req.GroupIDs,
"message": fmt.Sprintf("User erstellt: %s (%s)", req.Username, req.Email),
- }, ipAddress, userAgent)
+ }
+ if req.IsAdmin {
+ auditDetails["isAdmin"] = true
+ auditDetails["message"] = fmt.Sprintf("Administrator erstellt: %s (%s)", req.Username, req.Email)
+ }
+ auditService.Track(r.Context(), "CREATE", "user", userID, requestUserID, requestUsername, auditDetails, ipAddress, userAgent)
}
func updateUserHandler(w http.ResponseWriter, r *http.Request) {
@@ -3100,25 +3227,105 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
defer cancel()
// Prüfe ob Benutzer existiert
- var exists bool
- err := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE id = ?)", userID).Scan(&exists)
- if err != nil || !exists {
- http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
+ var isAdmin int
+ var currentUsername, currentEmail string
+ err := db.QueryRowContext(ctx, "SELECT is_admin, username, email FROM users WHERE id = ?", userID).
+ Scan(&isAdmin, ¤tUsername, ¤tEmail)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ http.Error(w, "Benutzer nicht gefunden", http.StatusNotFound)
+ return
+ }
+ http.Error(w, "Fehler beim Abrufen des Benutzers", http.StatusInternalServerError)
+ log.Printf("Fehler beim Abrufen des Benutzers: %v", err)
return
}
+ // Nur der spezielle Admin-User mit UID "admin": Username und Email sind unveränderbar
+ // Andere Admin-User können ihre Daten ändern
+ if userID == "admin" {
+ if req.Username != "" && req.Username != currentUsername {
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden"})
+ return
+ }
+ if req.Email != "" && req.Email != currentEmail {
+ w.WriteHeader(http.StatusBadRequest)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden"})
+ return
+ }
+ }
+
// Update Felder
updates := []string{}
args := []interface{}{}
- if req.Username != "" {
+ // Nur Username/Email updaten wenn nicht der spezielle Admin-User mit UID "admin"
+ // Andere Admin-User können ihre Daten ändern
+ if req.Username != "" && (userID != "admin" || req.Username == currentUsername) {
updates = append(updates, "username = ?")
args = append(args, req.Username)
}
- if req.Email != "" {
+ if req.Email != "" && (userID != "admin" || req.Email == currentEmail) {
updates = append(updates, "email = ?")
args = append(args, req.Email)
}
+
+ // isAdmin aktualisieren, falls angegeben
+ // UID 'admin' kann seinen Admin-Status nicht ändern
+ if req.IsAdmin != nil {
+ // UID 'admin' ist immer Admin und kann nicht geändert werden
+ if userID == "admin" {
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Der Admin-Status des Users mit UID 'admin' kann nicht geändert werden"})
+ return
+ }
+
+ adminValue := 0
+ if *req.IsAdmin {
+ adminValue = 1
+ }
+ updates = append(updates, "is_admin = ?")
+ args = append(args, adminValue)
+
+ // Wenn Admin aktiviert wird, entferne alle Gruppen-Zuweisungen
+ if *req.IsAdmin {
+ _, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID)
+ if err != nil {
+ log.Printf("Fehler beim Löschen der Gruppen-Zuweisungen für Admin: %v", err)
+ }
+ }
+ }
+
+ // enabled aktualisieren, falls angegeben
+ // Nur der spezielle Admin-User mit UID "admin" kann enabled geändert werden
+ if req.Enabled != nil {
+ // Nur UID "admin" kann enabled geändert werden
+ if userID != "admin" {
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Nur der Admin-User mit UID 'admin' kann aktiviert/deaktiviert werden"})
+ return
+ }
+
+ // Prüfe ob der anfragende User ein Admin ist (für Deaktivierung)
+ if !*req.Enabled {
+ requestUserID, _ := getUserFromRequest(r)
+ isRequestingAdmin, err := isUserAdmin(requestUserID)
+ if err != nil || !isRequestingAdmin {
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Nur Administratoren können den Admin-User mit UID 'admin' deaktivieren"})
+ return
+ }
+ }
+
+ enabledValue := 0
+ if *req.Enabled {
+ enabledValue = 1
+ }
+ updates = append(updates, "enabled = ?")
+ args = append(args, enabledValue)
+ }
+
if req.Password != "" {
// Altes Passwort ist erforderlich, wenn Passwort geändert wird
if req.OldPassword == "" {
@@ -3185,22 +3392,36 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
}
// Aktualisiere Gruppen-Zuweisungen, falls angegeben
+ // Nur wenn User nicht Admin ist oder Admin deaktiviert wird
if req.GroupIDs != nil {
- // Lösche alle bestehenden Gruppen-Zuweisungen
- _, err = db.ExecContext(ctx, "DELETE FROM user_groups WHERE user_id = ?", userID)
- if err != nil {
- log.Printf("Fehler beim Löschen der Gruppen-Zuweisungen: %v", err)
+ // Prüfe ob User nach Update Admin ist
+ var willBeAdmin int
+ if req.IsAdmin != nil {
+ if *req.IsAdmin {
+ willBeAdmin = 1
+ }
+ } else {
+ willBeAdmin = isAdmin
}
- // 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)
+ // 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)
+ }
}
}
}
@@ -3208,16 +3429,21 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
// Lade aktualisierten Benutzer
var user User
- err = db.QueryRowContext(ctx, "SELECT id, username, email, created_at FROM users WHERE id = ?", userID).
- Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
+ var isAdminUpdated, enabledUpdated int
+ err = db.QueryRowContext(ctx, "SELECT id, username, email, is_admin, enabled, created_at FROM users WHERE id = ?", userID).
+ Scan(&user.ID, &user.Username, &user.Email, &isAdminUpdated, &enabledUpdated, &user.CreatedAt)
if err != nil {
http.Error(w, "Fehler beim Abrufen des aktualisierten Benutzers", http.StatusInternalServerError)
log.Printf("Fehler beim Abrufen des aktualisierten Benutzers: %v", err)
return
}
+ user.IsAdmin = isAdminUpdated == 1
+ user.Enabled = enabledUpdated == 1
- // Lade Gruppen-IDs
- if req.GroupIDs != nil {
+ // 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)
@@ -3231,6 +3457,8 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
}
groupRows.Close()
user.GroupIDs = groupIDs
+ } else {
+ user.GroupIDs = []string{}
}
}
@@ -3249,6 +3477,22 @@ func updateUserHandler(w http.ResponseWriter, r *http.Request) {
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
}
@@ -3272,6 +3516,29 @@ func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
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)
@@ -3294,9 +3561,9 @@ func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(response)
// Audit-Log: User gelöscht
- userID, username := getUserFromRequest(r)
+ requestUserID, username := getUserFromRequest(r)
ipAddress, userAgent := getRequestInfo(r)
- auditService.Track(r.Context(), "DELETE", "user", vars["id"], userID, username, map[string]interface{}{
+ auditService.Track(r.Context(), "DELETE", "user", vars["id"], requestUserID, username, map[string]interface{}{
"message": fmt.Sprintf("User gelöscht: %s", vars["id"]),
}, ipAddress, userAgent)
}
@@ -3952,7 +4219,8 @@ func getUserFromRequest(r *http.Request) (userID, username string) {
defer cancel()
var id string
- err = db.QueryRowContext(ctx, "SELECT id FROM users WHERE username = ?", username).Scan(&id)
+ var enabled int
+ err = db.QueryRowContext(ctx, "SELECT id, enabled FROM users WHERE username = ?", username).Scan(&id, &enabled)
if err != nil {
// Logge Fehler nur wenn es nicht "no rows" ist
if err != sql.ErrNoRows {
@@ -3961,6 +4229,12 @@ func getUserFromRequest(r *http.Request) (userID, username string) {
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
}
@@ -3978,6 +4252,27 @@ type PermissionGroupInfo struct {
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 == "" {
@@ -3987,6 +4282,19 @@ func getUserPermissions(userID string) (*UserPermissionInfo, error) {
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
@@ -4071,6 +4379,12 @@ func hasSpaceAccess(userID, spaceID string) (bool, error) {
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
@@ -4105,6 +4419,12 @@ func hasPermission(userID, spaceID string, requiredPermission PermissionLevel) (
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
@@ -4605,7 +4925,8 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
defer cancel()
var storedHash string
- err = db.QueryRowContext(ctx, "SELECT password_hash FROM users WHERE username = ?", username).Scan(&storedHash)
+ var enabled int
+ err = db.QueryRowContext(ctx, "SELECT password_hash, enabled FROM users WHERE username = ?", username).Scan(&storedHash, &enabled)
if err != nil {
if err == sql.ErrNoRows {
if !isAjaxRequest {
@@ -4623,6 +4944,17 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return
}
+ // Prüfe ob User aktiviert ist
+ if enabled == 0 {
+ if !isAjaxRequest {
+ w.Header().Set("WWW-Authenticate", `Basic realm="Certigo Addon"`)
+ }
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Benutzerkonto ist deaktiviert"})
+ return
+ }
+
// Prüfe Passwort
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
if err != nil {
@@ -4640,6 +4972,37 @@ func basicAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
}
}
+// adminOnlyMiddleware prüft, ob der Benutzer ein Admin ist
+func adminOnlyMiddleware(next http.HandlerFunc) http.HandlerFunc {
+ return basicAuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
+ userID, _ := getUserFromRequest(r)
+ if userID == "" {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusUnauthorized)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Nicht authentifiziert"})
+ return
+ }
+
+ isAdmin, err := isUserAdmin(userID)
+ if err != nil {
+ log.Printf("Fehler beim Prüfen des Admin-Status: %v", err)
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusInternalServerError)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Fehler beim Prüfen der Berechtigung"})
+ return
+ }
+
+ if !isAdmin {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Nur Administratoren haben Zugriff auf diese Funktion"})
+ return
+ }
+
+ next(w, r)
+ })
+}
+
// Login Handler für Frontend (validiert Basic Auth und gibt User-Info zurück)
func loginHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
@@ -4697,8 +5060,9 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
var user User
var storedHash string
- err = db.QueryRowContext(ctx, "SELECT id, username, email, password_hash, created_at FROM users WHERE username = ?", username).
- Scan(&user.ID, &user.Username, &user.Email, &storedHash, &user.CreatedAt)
+ var enabled int
+ err = db.QueryRowContext(ctx, "SELECT id, username, email, password_hash, enabled, created_at FROM users WHERE username = ?", username).
+ Scan(&user.ID, &user.Username, &user.Email, &storedHash, &enabled, &user.CreatedAt)
if err != nil {
if err == sql.ErrNoRows {
log.Printf("Benutzer nicht gefunden: %s", username)
@@ -4712,6 +5076,14 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
return
}
+ // Prüfe ob User aktiviert ist
+ if enabled == 0 {
+ log.Printf("Login-Versuch für deaktivierten Benutzer: %s", username)
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(map[string]string{"error": "Benutzerkonto ist deaktiviert"})
+ return
+ }
+
// Prüfe Passwort
err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
if err != nil {
@@ -4780,22 +5152,22 @@ func main() {
api.HandleFunc("/spaces/{spaceId}/fqdns/{fqdnId}/csr", basicAuthMiddleware(getCSRByFQDNHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/csrs", basicAuthMiddleware(deleteAllCSRsHandler)).Methods("DELETE", "OPTIONS")
- // User Routes
- api.HandleFunc("/users", getUsersHandler).Methods("GET", "OPTIONS")
- api.HandleFunc("/users", createUserHandler).Methods("POST", "OPTIONS")
- api.HandleFunc("/users/{id}", getUserHandler).Methods("GET", "OPTIONS")
- api.HandleFunc("/users/{id}", updateUserHandler).Methods("PUT", "OPTIONS")
- api.HandleFunc("/users/{id}", deleteUserHandler).Methods("DELETE", "OPTIONS")
+ // User Routes (Admin only)
+ api.HandleFunc("/users", adminOnlyMiddleware(getUsersHandler)).Methods("GET", "OPTIONS")
+ api.HandleFunc("/users", adminOnlyMiddleware(createUserHandler)).Methods("POST", "OPTIONS")
+ api.HandleFunc("/users/{id}", adminOnlyMiddleware(getUserHandler)).Methods("GET", "OPTIONS")
+ api.HandleFunc("/users/{id}", adminOnlyMiddleware(updateUserHandler)).Methods("PUT", "OPTIONS")
+ api.HandleFunc("/users/{id}", adminOnlyMiddleware(deleteUserHandler)).Methods("DELETE", "OPTIONS")
api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(getAvatarHandler)).Methods("GET", "OPTIONS")
api.HandleFunc("/users/{id}/avatar", basicAuthMiddleware(uploadAvatarHandler)).Methods("POST", "OPTIONS")
api.HandleFunc("/user/permissions", basicAuthMiddleware(getUserPermissionsHandler)).Methods("GET", "OPTIONS")
- // Permission Groups Routes (Protected)
- api.HandleFunc("/permission-groups", basicAuthMiddleware(getPermissionGroupsHandler)).Methods("GET", "OPTIONS")
- api.HandleFunc("/permission-groups", basicAuthMiddleware(createPermissionGroupHandler)).Methods("POST", "OPTIONS")
- api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(getPermissionGroupHandler)).Methods("GET", "OPTIONS")
- api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(updatePermissionGroupHandler)).Methods("PUT", "OPTIONS")
- api.HandleFunc("/permission-groups/{id}", basicAuthMiddleware(deletePermissionGroupHandler)).Methods("DELETE", "OPTIONS")
+ // 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")
diff --git a/backend/spaces.db-shm b/backend/spaces.db-shm
index 816ec62..99be2a6 100644
Binary files a/backend/spaces.db-shm and b/backend/spaces.db-shm differ
diff --git a/backend/spaces.db-wal b/backend/spaces.db-wal
index 5c13d9c..06c95e9 100644
Binary files a/backend/spaces.db-wal and b/backend/spaces.db-wal differ
diff --git a/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png b/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png
index 803e549..d601cd8 100644
Binary files a/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png and b/backend/uploads/avatars/7124facd-9b11-40d1-83b1-ca747f2a8b0f.png differ
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 8f216b4..8ef5d4c 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,6 +1,7 @@
import { useState } from 'react'
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider, useAuth } from './contexts/AuthContext'
+import { PermissionsProvider, usePermissions } from './contexts/PermissionsContext'
import Sidebar from './components/Sidebar'
import Footer from './components/Footer'
import Home from './pages/Home'
@@ -34,6 +35,43 @@ const ProtectedRoute = ({ children }) => {
return isAuthenticated ? children :
Lade...
+Zugriff verweigert
+Nur Administratoren haben Zugriff auf diese Seite.
+Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden
+ )}Die E-Mail-Adresse des Admin-Users mit UID 'admin' kann nicht geändert werden
+ )}Der Benutzername des Admin-Users mit UID 'admin' kann nicht geändert werden
+ )}✓ Passwörter stimmen überein
)}Keine Berechtigungsgruppen vorhanden
) : (