Implements a master toggle for security features across config, CLI, and application logic. This allows granular control over whether the advanced security middleware and protections are active globally.
174 lines
5.2 KiB
Go
174 lines
5.2 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"warpbox/lib/alerts"
|
|
"warpbox/lib/config"
|
|
"warpbox/lib/security"
|
|
)
|
|
|
|
const adminSessionCookie = "warpbox_admin_session"
|
|
const adminSessionMarker = "1"
|
|
|
|
func (app *App) adminLoginEnabled() bool {
|
|
return app.config.AdminLoginEnabled(app.config.AdminPassword != "")
|
|
}
|
|
|
|
func (app *App) adminAuthMiddleware(ctx *gin.Context) {
|
|
if !app.adminLoginEnabled() {
|
|
ctx.Redirect(http.StatusSeeOther, "/")
|
|
ctx.Abort()
|
|
return
|
|
}
|
|
|
|
token, err := ctx.Cookie(adminSessionCookie)
|
|
if err != nil || token != app.adminSessionToken() {
|
|
ctx.Redirect(http.StatusSeeOther, "/admin/login")
|
|
ctx.Abort()
|
|
return
|
|
}
|
|
|
|
ctx.Next()
|
|
}
|
|
|
|
func (app *App) adminSessionToken() string {
|
|
// A simple deterministic token derived from the admin credentials.
|
|
// This will improve when proper user/session storage is added.
|
|
return app.config.AdminUsername + ":" + app.config.AdminPassword
|
|
}
|
|
|
|
func (app *App) handleAdminLogin(ctx *gin.Context) {
|
|
if !app.adminLoginEnabled() {
|
|
ctx.Redirect(http.StatusSeeOther, "/")
|
|
return
|
|
}
|
|
|
|
// Already logged in.
|
|
if token, err := ctx.Cookie(adminSessionCookie); err == nil && token == app.adminSessionToken() {
|
|
ctx.Redirect(http.StatusSeeOther, "/admin/dashboard")
|
|
return
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, "admin/login.html", gin.H{})
|
|
}
|
|
|
|
func (app *App) handleAdminLoginPost(ctx *gin.Context) {
|
|
if !app.adminLoginEnabled() {
|
|
ctx.Redirect(http.StatusSeeOther, "/")
|
|
return
|
|
}
|
|
ip := app.clientIP(ctx)
|
|
guard := app.securityGuard
|
|
if app.securityFeaturesEnabled() && guard == nil {
|
|
guard = security.NewGuard()
|
|
app.securityGuard = guard
|
|
}
|
|
if app.securityFeaturesEnabled() && guard != nil && !guard.IsAdminWhitelisted(ip) && guard.IsBanned(ip) {
|
|
app.logActivity("auth.admin.block", "high", "Blocked admin login from banned IP", ctx, nil)
|
|
ctx.HTML(http.StatusTooManyRequests, "admin/login.html", gin.H{
|
|
"ErrorMessage": "Too many failed attempts. Try again later.",
|
|
})
|
|
return
|
|
}
|
|
|
|
username := strings.TrimSpace(ctx.PostForm("username"))
|
|
password := ctx.PostForm("password")
|
|
|
|
if username != app.config.AdminUsername || password != app.config.AdminPassword {
|
|
if app.securityFeaturesEnabled() && guard != nil && !guard.IsAdminWhitelisted(ip) {
|
|
banned, attempts := guard.RegisterFailedLogin(ip, app.config.SecurityLoginWindowSeconds, app.config.SecurityLoginMaxAttempts, app.config.SecurityBanSeconds)
|
|
app.logActivity("auth.admin.failed", "medium", "Failed admin login", ctx, map[string]string{"attempts": strconv.Itoa(attempts)})
|
|
if banned {
|
|
app.createAlert("Admin login brute-force blocked", "high", "security", "401", "auth.admin.bruteforce", "Too many failed admin logins triggered temporary ban.", map[string]string{"ip": ip, "attempts": strconv.Itoa(attempts)})
|
|
app.logActivity("security.ban", "high", "Auto-banned IP after admin login failures", ctx, map[string]string{"attempts": strconv.Itoa(attempts)})
|
|
}
|
|
}
|
|
ctx.HTML(http.StatusUnauthorized, "admin/login.html", gin.H{
|
|
"ErrorMessage": "Invalid username or password.",
|
|
})
|
|
return
|
|
}
|
|
|
|
app.logActivity("auth.admin.success", "low", "Admin login successful", ctx, nil)
|
|
secure := app.config.AdminCookieSecure
|
|
maxAge := int(app.config.SessionTTLSeconds)
|
|
|
|
ctx.SetCookie(adminSessionCookie, app.adminSessionToken(), maxAge, "/admin", "", secure, true)
|
|
ctx.Redirect(http.StatusSeeOther, "/admin/dashboard")
|
|
}
|
|
|
|
func (app *App) handleAdminLogout(ctx *gin.Context) {
|
|
secure := app.config.AdminCookieSecure
|
|
ctx.SetCookie(adminSessionCookie, "", -1, "/admin", "", secure, true)
|
|
ctx.Redirect(http.StatusSeeOther, "/admin/login")
|
|
}
|
|
|
|
func (app *App) handleAdminDashboard(ctx *gin.Context) {
|
|
if !app.adminLoginEnabled() {
|
|
ctx.Redirect(http.StatusSeeOther, "/")
|
|
return
|
|
}
|
|
|
|
dashboardEnabled := config.AdminEnabledTrue
|
|
if cfgVal := app.config.AdminEnabled; cfgVal == config.AdminEnabledAuto || cfgVal == config.AdminEnabledTrue {
|
|
dashboardEnabled = cfgVal
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, "admin/dashboard.html", gin.H{
|
|
"AdminUsername": app.config.AdminUsername,
|
|
"AdminEmail": app.config.AdminEmail,
|
|
"ActivePage": "dashboard",
|
|
"DashboardEnabled": string(dashboardEnabled),
|
|
})
|
|
}
|
|
|
|
func (app *App) handleAdminAlerts(ctx *gin.Context) {
|
|
if !app.adminLoginEnabled() {
|
|
ctx.Redirect(http.StatusSeeOther, "/")
|
|
return
|
|
}
|
|
|
|
alertsList := []alerts.Alert{}
|
|
if app.alertStore != nil {
|
|
var err error
|
|
alertsList, err = app.alertStore.List(500)
|
|
if err != nil {
|
|
ctx.String(http.StatusInternalServerError, "Could not load alerts")
|
|
return
|
|
}
|
|
}
|
|
openCount := 0
|
|
highCount := 0
|
|
ackedCount := 0
|
|
closedCount := 0
|
|
for _, alert := range alertsList {
|
|
switch string(alert.Status) {
|
|
case "open":
|
|
openCount++
|
|
case "acked":
|
|
ackedCount++
|
|
case "closed":
|
|
closedCount++
|
|
}
|
|
if alert.Severity == "high" && string(alert.Status) != "closed" {
|
|
highCount++
|
|
}
|
|
}
|
|
|
|
ctx.HTML(http.StatusOK, "admin/alerts.html", gin.H{
|
|
"AdminUsername": app.config.AdminUsername,
|
|
"AdminEmail": app.config.AdminEmail,
|
|
"ActivePage": "alerts",
|
|
"Alerts": alertsList,
|
|
"OpenCount": strconv.Itoa(openCount),
|
|
"HighCount": strconv.Itoa(highCount),
|
|
"AckCount": strconv.Itoa(ackedCount),
|
|
"ClosedCount": strconv.Itoa(closedCount),
|
|
})
|
|
}
|