Non-admin users are now restricted to servers explicitly listed in their new ServerIDs field; empty means no access (secure by default). Admins always have full access. Migration backfills existing users' ServerIDs with the migrated legacy server so nobody is locked out on upgrade. New RequireServerAccess middleware enforces this on /servers/:id/... routes (applied to GET /servers/:id/clients so far); GET /servers also filters its list for non-admins.
279 lines
6.9 KiB
Go
279 lines
6.9 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gorilla/sessions"
|
|
"github.com/labstack/echo-contrib/session"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/ngoduykhanh/wireguard-ui/store"
|
|
"github.com/ngoduykhanh/wireguard-ui/util"
|
|
)
|
|
|
|
func ValidSession(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if !isValidSession(c) {
|
|
nextURL := c.Request().URL
|
|
if nextURL != nil && c.Request().Method == http.MethodGet {
|
|
return c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf(util.BasePath+"/login?next=%s", c.Request().URL))
|
|
} else {
|
|
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/login")
|
|
}
|
|
}
|
|
return next(c)
|
|
}
|
|
}
|
|
|
|
// RefreshSession must only be used after ValidSession middleware
|
|
// RefreshSession checks if the session is eligible for the refresh, but doesn't check if it's fully valid
|
|
func RefreshSession(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
doRefreshSession(c)
|
|
return next(c)
|
|
}
|
|
}
|
|
|
|
func NeedsAdmin(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if !isAdmin(c) {
|
|
return c.Redirect(http.StatusTemporaryRedirect, util.BasePath+"/")
|
|
}
|
|
return next(c)
|
|
}
|
|
}
|
|
|
|
// RequireServerAccess must only be used after ValidSession middleware.
|
|
// It restricts /servers/:id/... routes to admins (always allowed) and to
|
|
// non-admin users whose User.ServerIDs contains the requested server ID.
|
|
// A user with an empty ServerIDs list has no server access at all - the
|
|
// migration backfills existing users so nobody is locked out by this
|
|
// change on upgrade.
|
|
func RequireServerAccess(db store.IStore) echo.MiddlewareFunc {
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if util.DisableLogin || isAdmin(c) {
|
|
return next(c)
|
|
}
|
|
|
|
serverID := c.Param("id")
|
|
user, err := db.GetUserByName(currentUser(c))
|
|
if err != nil {
|
|
return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "Access denied"})
|
|
}
|
|
for _, id := range user.ServerIDs {
|
|
if id == serverID {
|
|
return next(c)
|
|
}
|
|
}
|
|
return c.JSON(http.StatusForbidden, jsonHTTPResponse{false, "You do not have access to this server"})
|
|
}
|
|
}
|
|
}
|
|
|
|
func isValidSession(c echo.Context) bool {
|
|
if util.DisableLogin {
|
|
return true
|
|
}
|
|
sess, _ := session.Get("session", c)
|
|
cookie, err := c.Cookie("session_token")
|
|
if err != nil || sess.Values["session_token"] != cookie.Value {
|
|
return false
|
|
}
|
|
|
|
// Check time bounds
|
|
createdAt := getCreatedAt(sess)
|
|
updatedAt := getUpdatedAt(sess)
|
|
maxAge := getMaxAge(sess)
|
|
// Temporary session is considered valid within 24h if browser is not closed before
|
|
// This value is not saved and is used as virtual expiration
|
|
if maxAge == 0 {
|
|
maxAge = 86400
|
|
}
|
|
expiration := updatedAt + int64(maxAge)
|
|
now := time.Now().UTC().Unix()
|
|
if updatedAt > now || expiration < now || createdAt+util.SessionMaxDuration < now {
|
|
return false
|
|
}
|
|
|
|
// Check if user still exists and unchanged
|
|
username := fmt.Sprintf("%s", sess.Values["username"])
|
|
userHash := getUserHash(sess)
|
|
if uHash, ok := util.DBUsersToCRC32[username]; !ok || userHash != uHash {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Refreshes a "remember me" session when the user visits web pages (not API)
|
|
// Session must be valid before calling this function
|
|
// Refresh is performed at most once per 24h
|
|
func doRefreshSession(c echo.Context) {
|
|
if util.DisableLogin {
|
|
return
|
|
}
|
|
|
|
sess, _ := session.Get("session", c)
|
|
maxAge := getMaxAge(sess)
|
|
if maxAge <= 0 {
|
|
return
|
|
}
|
|
|
|
oldCookie, err := c.Cookie("session_token")
|
|
if err != nil || sess.Values["session_token"] != oldCookie.Value {
|
|
return
|
|
}
|
|
|
|
// Refresh no sooner than 24h
|
|
createdAt := getCreatedAt(sess)
|
|
updatedAt := getUpdatedAt(sess)
|
|
expiration := updatedAt + int64(getMaxAge(sess))
|
|
now := time.Now().UTC().Unix()
|
|
if updatedAt > now || expiration < now || now-updatedAt < 86_400 || createdAt+util.SessionMaxDuration < now {
|
|
return
|
|
}
|
|
|
|
cookiePath := util.GetCookiePath()
|
|
|
|
sess.Values["updated_at"] = now
|
|
sess.Options = &sessions.Options{
|
|
Path: cookiePath,
|
|
MaxAge: maxAge,
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
}
|
|
sess.Save(c.Request(), c.Response())
|
|
|
|
cookie := new(http.Cookie)
|
|
cookie.Name = "session_token"
|
|
cookie.Path = cookiePath
|
|
cookie.Value = oldCookie.Value
|
|
cookie.MaxAge = maxAge
|
|
cookie.HttpOnly = true
|
|
cookie.SameSite = http.SameSiteLaxMode
|
|
c.SetCookie(cookie)
|
|
}
|
|
|
|
// Get time in seconds this session is valid without updating
|
|
func getMaxAge(sess *sessions.Session) int {
|
|
if util.DisableLogin {
|
|
return 0
|
|
}
|
|
|
|
maxAge := sess.Values["max_age"]
|
|
|
|
switch typedMaxAge := maxAge.(type) {
|
|
case int:
|
|
return typedMaxAge
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// Get a timestamp in seconds of the time the session was created
|
|
func getCreatedAt(sess *sessions.Session) int64 {
|
|
if util.DisableLogin {
|
|
return 0
|
|
}
|
|
|
|
createdAt := sess.Values["created_at"]
|
|
|
|
switch typedCreatedAt := createdAt.(type) {
|
|
case int64:
|
|
return typedCreatedAt
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// Get a timestamp in seconds of the last session update
|
|
func getUpdatedAt(sess *sessions.Session) int64 {
|
|
if util.DisableLogin {
|
|
return 0
|
|
}
|
|
|
|
lastUpdate := sess.Values["updated_at"]
|
|
|
|
switch typedLastUpdate := lastUpdate.(type) {
|
|
case int64:
|
|
return typedLastUpdate
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// Get CRC32 of a user at the moment of log in
|
|
// Any changes to user will result in logout of other (not updated) sessions
|
|
func getUserHash(sess *sessions.Session) uint32 {
|
|
if util.DisableLogin {
|
|
return 0
|
|
}
|
|
|
|
userHash := sess.Values["user_hash"]
|
|
|
|
switch typedUserHash := userHash.(type) {
|
|
case uint32:
|
|
return typedUserHash
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// currentUser to get username of logged in user
|
|
func currentUser(c echo.Context) string {
|
|
if util.DisableLogin {
|
|
return ""
|
|
}
|
|
|
|
sess, _ := session.Get("session", c)
|
|
username := fmt.Sprintf("%s", sess.Values["username"])
|
|
return username
|
|
}
|
|
|
|
// isAdmin to get user type: admin or manager
|
|
func isAdmin(c echo.Context) bool {
|
|
if util.DisableLogin {
|
|
return true
|
|
}
|
|
|
|
sess, _ := session.Get("session", c)
|
|
admin := fmt.Sprintf("%t", sess.Values["admin"])
|
|
return admin == "true"
|
|
}
|
|
|
|
func setUser(c echo.Context, username string, admin bool, userCRC32 uint32) {
|
|
sess, _ := session.Get("session", c)
|
|
sess.Values["username"] = username
|
|
sess.Values["user_hash"] = userCRC32
|
|
sess.Values["admin"] = admin
|
|
sess.Save(c.Request(), c.Response())
|
|
}
|
|
|
|
// clearSession to remove current session
|
|
func clearSession(c echo.Context) {
|
|
sess, _ := session.Get("session", c)
|
|
sess.Values["username"] = ""
|
|
sess.Values["user_hash"] = 0
|
|
sess.Values["admin"] = false
|
|
sess.Values["session_token"] = ""
|
|
sess.Values["max_age"] = -1
|
|
sess.Options.MaxAge = -1
|
|
sess.Save(c.Request(), c.Response())
|
|
|
|
cookiePath := util.GetCookiePath()
|
|
|
|
cookie, err := c.Cookie("session_token")
|
|
if err != nil {
|
|
cookie = new(http.Cookie)
|
|
}
|
|
|
|
cookie.Name = "session_token"
|
|
cookie.Path = cookiePath
|
|
cookie.MaxAge = -1
|
|
cookie.HttpOnly = true
|
|
cookie.SameSite = http.SameSiteLaxMode
|
|
c.SetCookie(cookie)
|
|
}
|