174 lines
5.0 KiB
Go
174 lines
5.0 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 := clientIP(ctx)
|
|
guard := app.securityGuard
|
|
if guard == nil {
|
|
guard = security.NewGuard()
|
|
app.securityGuard = guard
|
|
}
|
|
if !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 !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),
|
|
})
|
|
}
|