Adds configuration options and environment variables to manage box owner policies, including settings for refresh counts and expiry.
196 lines
5.2 KiB
Go
196 lines
5.2 KiB
Go
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")
|
|
if token == "" {
|
|
token = ctx.GetHeader("X-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
|
|
}
|