5758 lines
172 KiB
Go
5758 lines
172 KiB
Go
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 der Benutzer FULL_ACCESS hat (ohne Space-Beschränkung)
|
|
permissions, err := getUserPermissions(userID)
|
|
if err != nil || len(permissions.Groups) == 0 {
|
|
http.Error(w, "Keine Berechtigung zum Erstellen von Spaces", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
hasFullAccess := false
|
|
for _, group := range permissions.Groups {
|
|
if group.Permission == PermissionFullAccess {
|
|
hasFullAccess = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasFullAccess {
|
|
http.Error(w, "Keine Berechtigung zum Erstellen von Spaces. Vollzugriff erforderlich.", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
var req CreateSpaceRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
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
|
|
}
|
|
|
|
permissions, err := getUserPermissions(userID)
|
|
if err != nil || len(permissions.Groups) == 0 {
|
|
http.Error(w, "Keine Berechtigung zum Löschen aller FQDNs. Vollzugriff erforderlich.", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
hasFullAccess := false
|
|
for _, group := range permissions.Groups {
|
|
if group.Permission == PermissionFullAccess {
|
|
hasFullAccess = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasFullAccess {
|
|
http.Error(w, "Keine Berechtigung zum Löschen aller FQDNs. Vollzugriff erforderlich.", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// Prüfe Query-Parameter für Bestätigung (Sicherheitsmaßnahme)
|
|
confirm := r.URL.Query().Get("confirm")
|
|
if confirm != "true" {
|
|
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
|
|
}
|
|
|
|
permissions, err := getUserPermissions(userID)
|
|
if err != nil || len(permissions.Groups) == 0 {
|
|
http.Error(w, "Keine Berechtigung zum Löschen aller CSRs. Vollzugriff erforderlich.", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
hasFullAccess := false
|
|
for _, group := range permissions.Groups {
|
|
if group.Permission == PermissionFullAccess {
|
|
hasFullAccess = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasFullAccess {
|
|
http.Error(w, "Keine Berechtigung zum Löschen aller CSRs. Vollzugriff erforderlich.", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// Prüfe Query-Parameter für Bestätigung (Sicherheitsmaßnahme)
|
|
confirm := r.URL.Query().Get("confirm")
|
|
if confirm != "true" {
|
|
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 := `<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Certigo Addon API - Swagger UI</title>
|
|
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
|
|
<style>
|
|
html {
|
|
box-sizing: border-box;
|
|
overflow: -moz-scrollbars-vertical;
|
|
overflow-y: scroll;
|
|
}
|
|
*, *:before, *:after {
|
|
box-sizing: inherit;
|
|
}
|
|
body {
|
|
margin:0;
|
|
background: #fafafa;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="swagger-ui"></div>
|
|
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
|
|
<script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
|
|
<script>
|
|
window.onload = function() {
|
|
const ui = SwaggerUIBundle({
|
|
url: "/api/openapi.yaml",
|
|
dom_id: '#swagger-ui',
|
|
deepLinking: true,
|
|
presets: [
|
|
SwaggerUIBundle.presets.apis,
|
|
SwaggerUIStandalonePreset
|
|
],
|
|
plugins: [
|
|
SwaggerUIBundle.plugins.DownloadUrl
|
|
],
|
|
layout: "StandaloneLayout"
|
|
});
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>`
|
|
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, hat er keinen Zugriff
|
|
if len(permissions.Groups) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
// Prüfe für jede Gruppe, ob der Benutzer Zugriff auf den Space hat
|
|
for _, group := range permissions.Groups {
|
|
// Wenn die Gruppe keine Spaces zugewiesen hat, hat der Benutzer Zugriff auf alle Spaces
|
|
if len(group.SpaceIDs) == 0 {
|
|
return true, nil
|
|
}
|
|
// Prüfe, ob der Space in der Liste der zugewiesenen Spaces ist
|
|
for _, assignedSpaceID := range group.SpaceIDs {
|
|
if assignedSpaceID == spaceID {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// hasPermission prüft, ob ein Benutzer eine bestimmte Berechtigung für einen Space hat
|
|
// requiredPermission kann READ, READ_WRITE oder FULL_ACCESS sein
|
|
func hasPermission(userID, spaceID string, requiredPermission PermissionLevel) (bool, error) {
|
|
if userID == "" {
|
|
return false, nil
|
|
}
|
|
|
|
// Admins haben immer alle Berechtigungen
|
|
isAdmin, err := isUserAdmin(userID)
|
|
if err == nil && isAdmin {
|
|
return true, nil
|
|
}
|
|
|
|
permissions, err := getUserPermissions(userID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// Wenn der Benutzer keine Gruppen hat, hat er keine Berechtigung
|
|
if len(permissions.Groups) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
// Prüfe für jede Gruppe
|
|
for _, group := range permissions.Groups {
|
|
hasAccess := false
|
|
|
|
// Prüfe, ob der Benutzer Zugriff auf den Space hat
|
|
if len(group.SpaceIDs) == 0 {
|
|
// Keine Space-Zuweisungen = Zugriff auf alle Spaces
|
|
hasAccess = true
|
|
} else {
|
|
// Prüfe, ob der Space in der Liste ist
|
|
for _, assignedSpaceID := range group.SpaceIDs {
|
|
if assignedSpaceID == spaceID {
|
|
hasAccess = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if !hasAccess {
|
|
continue
|
|
}
|
|
|
|
// Prüfe die Berechtigungsstufe
|
|
switch requiredPermission {
|
|
case PermissionRead:
|
|
// READ, READ_WRITE und FULL_ACCESS haben alle READ-Berechtigung
|
|
return true, nil
|
|
case PermissionReadWrite:
|
|
// READ_WRITE und FULL_ACCESS haben READ_WRITE-Berechtigung
|
|
if group.Permission == PermissionReadWrite || group.Permission == PermissionFullAccess {
|
|
return true, nil
|
|
}
|
|
case PermissionFullAccess:
|
|
// Nur FULL_ACCESS hat FULL_ACCESS-Berechtigung
|
|
if group.Permission == PermissionFullAccess {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// getAccessibleSpaceIDs gibt alle Space-IDs zurück, auf die der Benutzer Zugriff hat
|
|
func getAccessibleSpaceIDs(userID string) ([]string, error) {
|
|
if userID == "" {
|
|
return []string{}, nil
|
|
}
|
|
|
|
permissions, err := getUserPermissions(userID)
|
|
if err != nil {
|
|
return []string{}, err
|
|
}
|
|
|
|
// Wenn der Benutzer keine Gruppen hat, hat er keinen Zugriff
|
|
if len(permissions.Groups) == 0 {
|
|
return []string{}, nil
|
|
}
|
|
|
|
// Sammle alle zugewiesenen Spaces
|
|
spaceIDMap := make(map[string]bool)
|
|
hasUnrestrictedAccess := false
|
|
|
|
for _, group := range permissions.Groups {
|
|
// Wenn eine Gruppe keine Spaces zugewiesen hat, hat der Benutzer Zugriff auf alle Spaces
|
|
if len(group.SpaceIDs) == 0 {
|
|
hasUnrestrictedAccess = true
|
|
break
|
|
}
|
|
// Sammle alle zugewiesenen Spaces
|
|
for _, spaceID := range group.SpaceIDs {
|
|
spaceIDMap[spaceID] = true
|
|
}
|
|
}
|
|
|
|
// Wenn der Benutzer Zugriff auf alle Spaces hat, hole alle Spaces aus der DB
|
|
if hasUnrestrictedAccess {
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
|
defer cancel()
|
|
|
|
rows, err := db.QueryContext(ctx, "SELECT id FROM spaces")
|
|
if err != nil {
|
|
return []string{}, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var spaceIDs []string
|
|
for rows.Next() {
|
|
var spaceID string
|
|
if err := rows.Scan(&spaceID); err == nil {
|
|
spaceIDs = append(spaceIDs, spaceID)
|
|
}
|
|
}
|
|
return spaceIDs, nil
|
|
}
|
|
|
|
// Konvertiere Map zu Slice
|
|
spaceIDs := make([]string, 0, len(spaceIDMap))
|
|
for spaceID := range spaceIDMap {
|
|
spaceIDs = append(spaceIDs, spaceID)
|
|
}
|
|
|
|
return spaceIDs, nil
|
|
}
|
|
|
|
// Helper-Funktion zum Extrahieren von IP-Adresse und User-Agent aus Request
|
|
func getRequestInfo(r *http.Request) (ipAddress, userAgent string) {
|
|
// Hole IP-Adresse
|
|
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,
|
|
})
|
|
}
|