refactor: große API-Handler-Dateien in fokussierte Module aufteilen
Reine Code-Verschiebung, keine Logikänderung. Betroffen: - ldap_tenants.go (994 Zeilen) -> ldap_tenants.go (Routing) + ldap_handlers.go + tenant_handlers.go + tenant_domain_handlers.go + tenant_logo_handlers.go + tenant_helpers.go - import_handlers.go (621 Zeilen) -> imap_handlers.go + pop3_handlers.go + import_helpers.go - admin_handlers.go (702 Zeilen) -> admin_users_handlers.go + admin_status_handlers.go + admin_services_handlers.go + admin_security_handlers.go Lokal kein Go-Build möglich — Verifikation manuell per Funktions- und Import-Abgleich Alt/Neu (alle Symbole exakt einmal vorhanden). Build muss vor Deploy auf 192.168.1.131/132 bestätigt werden.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"archivmail/internal/audit"
|
||||
"archivmail/internal/auth"
|
||||
"archivmail/internal/userstore"
|
||||
)
|
||||
|
||||
// --- Service management ---
|
||||
|
||||
// allowedServices is the whitelist of systemd service names the admin may control.
|
||||
var allowedServices = []string{
|
||||
"archivmail",
|
||||
"archivmail-web",
|
||||
"postgresql@17-main",
|
||||
"postfix",
|
||||
"nginx",
|
||||
}
|
||||
|
||||
type ServiceStatus struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Active string `json:"active"` // active, inactive, failed, unknown
|
||||
Sub string `json:"sub"` // running, dead, exited, ...
|
||||
Enabled string `json:"enabled"` // enabled, disabled, static, unknown
|
||||
Description string `json:"description"`
|
||||
ExternalBlocked *bool `json:"external_blocked,omitempty"` // only set for archivmail
|
||||
}
|
||||
|
||||
func isAllowedService(name string) bool {
|
||||
for _, s := range allowedServices {
|
||||
if s == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func systemctlShow(name string) ServiceStatus {
|
||||
svc := ServiceStatus{Name: name, DisplayName: name}
|
||||
out, err := exec.Command("systemctl", "show", name+".service",
|
||||
"--property=ActiveState,SubState,UnitFileState,Description",
|
||||
"--no-pager").Output()
|
||||
if err != nil {
|
||||
svc.Active = "unknown"
|
||||
svc.Sub = ""
|
||||
svc.Enabled = "unknown"
|
||||
} else {
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
k, v, ok := strings.Cut(line, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch k {
|
||||
case "ActiveState":
|
||||
svc.Active = v
|
||||
case "SubState":
|
||||
svc.Sub = v
|
||||
case "UnitFileState":
|
||||
svc.Enabled = v
|
||||
case "Description":
|
||||
svc.Description = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if name == "archivmail" {
|
||||
blocked := nftAPIBlocked()
|
||||
svc.ExternalBlocked = &blocked
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
||||
// nftAPIBlocked reports whether external access to port 8080 is currently blocked.
|
||||
func nftAPIBlocked() bool {
|
||||
out, err := exec.Command("sudo", "/usr/local/sbin/archivmail-nft", "status").Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(string(out)) == "blocked"
|
||||
}
|
||||
|
||||
func (s *Server) handleListServices(w http.ResponseWriter, r *http.Request) {
|
||||
result := make([]ServiceStatus, 0, len(allowedServices))
|
||||
for _, name := range allowedServices {
|
||||
result = append(result, systemctlShow(name))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (s *Server) handleServiceAction(w http.ResponseWriter, r *http.Request) {
|
||||
// Only superadmin may start/stop/restart services
|
||||
sess := sessionFromCtx(r.Context())
|
||||
if sess == nil || !auth.HasRole(sess.Role, userstore.RoleSuperAdmin) {
|
||||
writeError(w, http.StatusForbidden, "superadmin required")
|
||||
return
|
||||
}
|
||||
|
||||
name := r.PathValue("name")
|
||||
if !isAllowedService(name) {
|
||||
writeError(w, http.StatusBadRequest, "unknown service")
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Action string `json:"action"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
allowedActions := map[string]bool{
|
||||
"start": true, "stop": true, "restart": true,
|
||||
"enable": true, "disable": true,
|
||||
}
|
||||
nftActions := map[string]string{
|
||||
"block_external": "block",
|
||||
"allow_external": "unblock",
|
||||
}
|
||||
|
||||
if nftArg, isNft := nftActions[body.Action]; isNft {
|
||||
if name != "archivmail" {
|
||||
writeError(w, http.StatusBadRequest, "external access control only available for archivmail")
|
||||
return
|
||||
}
|
||||
out, err := exec.Command("sudo", "/usr/local/sbin/archivmail-nft", nftArg).CombinedOutput()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, strings.TrimSpace(string(out)))
|
||||
return
|
||||
}
|
||||
sess := sessionFromCtx(r.Context())
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: "service." + body.Action,
|
||||
Username: sess.Username,
|
||||
TenantID: sess.TenantID,
|
||||
IPAddress: s.remoteIP(r),
|
||||
Detail: name,
|
||||
Success: true,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, systemctlShow(name))
|
||||
return
|
||||
}
|
||||
|
||||
if !allowedActions[body.Action] {
|
||||
writeError(w, http.StatusBadRequest, "unknown action")
|
||||
return
|
||||
}
|
||||
|
||||
out, err := exec.Command("sudo", "/usr/bin/systemctl", body.Action, name+".service").CombinedOutput()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, strings.TrimSpace(string(out)))
|
||||
return
|
||||
}
|
||||
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: "service." + body.Action,
|
||||
Username: sess.Username,
|
||||
TenantID: sess.TenantID,
|
||||
IPAddress: s.remoteIP(r),
|
||||
Detail: name,
|
||||
Success: true,
|
||||
})
|
||||
|
||||
writeJSON(w, http.StatusOK, systemctlShow(name))
|
||||
}
|
||||
Reference in New Issue
Block a user