package server import ( "crypto/subtle" "net/http" "strings" "time" "github.com/gin-gonic/gin" "warpbox/lib/metastore" ) const adminSessionCookie = "warpbox_admin_session" func (app *App) handleAdminLogin(ctx *gin.Context) { if app.isAdminSessionValid(ctx) { ctx.Redirect(http.StatusSeeOther, "/admin") return } app.renderAdminLogin(ctx, "") } func (app *App) handleAdminLoginPost(ctx *gin.Context) { if !app.adminLoginEnabled { app.renderAdminLogin(ctx, "Administrator login is disabled.") return } username := strings.TrimSpace(ctx.PostForm("username")) password := ctx.PostForm("password") user, ok, err := app.store.GetUserByUsername(username) if err != nil { ctx.String(http.StatusInternalServerError, "Could not load user") return } if !ok || user.Disabled || !metastore.VerifyPassword(user.PasswordHash, password) { app.renderAdminLogin(ctx, "The username or password was not accepted.") return } perms, err := app.permissionsForUser(user) if err != nil { ctx.String(http.StatusInternalServerError, "Could not load permissions") return } if !perms.AdminAccess { app.renderAdminLogin(ctx, "This user does not have administrator access.") return } session, err := app.store.CreateSession(user.ID, time.Duration(app.config.SessionTTLSeconds)*time.Second) if err != nil { ctx.String(http.StatusInternalServerError, "Could not create session") return } ctx.SetSameSite(http.SameSiteLaxMode) ctx.SetCookie(adminSessionCookie, session.Token, int(app.config.SessionTTLSeconds), "/admin", "", app.config.AdminCookieSecure, true) ctx.Redirect(http.StatusSeeOther, "/admin") } func (app *App) handleAdminLogout(ctx *gin.Context) { if token, err := ctx.Cookie(adminSessionCookie); err == nil { _ = app.store.DeleteSession(token) } ctx.SetSameSite(http.SameSiteLaxMode) ctx.SetCookie(adminSessionCookie, "", -1, "/admin", "", app.config.AdminCookieSecure, true) ctx.Redirect(http.StatusSeeOther, "/admin/login") } func (app *App) requireAdminSession(ctx *gin.Context) { token, err := ctx.Cookie(adminSessionCookie) if err != nil { ctx.Redirect(http.StatusSeeOther, "/admin/login") ctx.Abort() return } session, ok, err := app.store.GetSession(token) if err != nil || !ok { ctx.Redirect(http.StatusSeeOther, "/admin/login") ctx.Abort() return } if !validAdminCSRF(ctx, session) { ctx.String(http.StatusForbidden, "Permission denied") ctx.Abort() return } user, ok, err := app.store.GetUser(session.UserID) if err != nil || !ok || user.Disabled { ctx.Redirect(http.StatusSeeOther, "/admin/login") ctx.Abort() return } perms, err := app.permissionsForUser(user) if err != nil || !perms.AdminAccess { ctx.Redirect(http.StatusSeeOther, "/admin/login") ctx.Abort() return } ctx.Set("adminUser", user) ctx.Set("adminPerms", perms) ctx.Set("adminCSRFToken", session.CSRFToken) ctx.Next() } func (app *App) isAdminSessionValid(ctx *gin.Context) bool { token, err := ctx.Cookie(adminSessionCookie) if err != nil { return false } session, ok, err := app.store.GetSession(token) if err != nil || !ok { return false } user, ok, err := app.store.GetUser(session.UserID) if err != nil || !ok || user.Disabled { return false } perms, err := app.permissionsForUser(user) return err == nil && perms.AdminAccess } func (app *App) permissionsForUser(user metastore.User) (metastore.EffectivePermissions, error) { tags, err := app.store.TagsByID(user.TagIDs) if err != nil { return metastore.EffectivePermissions{}, err } return metastore.ResolveUserPermissions(app.config, user, tags), nil } func (app *App) requireAdminFlag(ctx *gin.Context, allowed func(metastore.EffectivePermissions) bool) bool { value, ok := ctx.Get("adminPerms") if !ok { ctx.String(http.StatusForbidden, "Permission denied") return false } perms, ok := value.(metastore.EffectivePermissions) if !ok || !allowed(perms) { ctx.String(http.StatusForbidden, "Permission denied") return false } return true } func (app *App) currentAdminUsername(ctx *gin.Context) string { if current, ok := ctx.Get("adminUser"); ok { if user, ok := current.(metastore.User); ok { return user.Username } } return "" } func (app *App) currentCSRFToken(ctx *gin.Context) string { if value, ok := ctx.Get("adminCSRFToken"); ok { if token, ok := value.(string); ok { return token } } return "" } func (app *App) renderAdminLogin(ctx *gin.Context, errorMessage string) { ctx.HTML(http.StatusOK, "admin_login.html", gin.H{ "AdminLoginEnabled": app.adminLoginEnabled, "Error": errorMessage, }) } func noStoreAdminHeaders(ctx *gin.Context) { ctx.Header("Cache-Control", "no-store") ctx.Header("Pragma", "no-cache") ctx.Header("X-Content-Type-Options", "nosniff") ctx.Next() } func validAdminCSRF(ctx *gin.Context, session metastore.Session) bool { switch ctx.Request.Method { case http.MethodGet, http.MethodHead, http.MethodOptions: return true } token := ctx.PostForm("csrf_token") return token != "" && subtleConstantTimeEqual(token, session.CSRFToken) } func subtleConstantTimeEqual(a string, b string) bool { if len(a) != len(b) { return false } return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 }