75 lines
1.8 KiB
Go
75 lines
1.8 KiB
Go
|
|
package auth
|
||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
|
||
|
|
"github.com/gin-gonic/gin"
|
||
|
|
|
||
|
|
"dragonrelay/internal/db"
|
||
|
|
)
|
||
|
|
|
||
|
|
// LevelRank returns a numeric rank for a host-access level so that levels can
|
||
|
|
// be compared ordinally. Unknown strings map to 0, which is below every named
|
||
|
|
// level.
|
||
|
|
func LevelRank(level string) int {
|
||
|
|
switch level {
|
||
|
|
case "view":
|
||
|
|
return 1
|
||
|
|
case "connect":
|
||
|
|
return 2
|
||
|
|
case "manage":
|
||
|
|
return 3
|
||
|
|
default:
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// RequireHostAccess returns a Gin middleware that enforces per-host ACL checks.
|
||
|
|
// The host IP is read from the ":ip" route parameter.
|
||
|
|
//
|
||
|
|
// Resolution order:
|
||
|
|
// 1. No claims in context → 401 Unauthorized
|
||
|
|
// 2. Role "admin" → always allowed (bypass ACL)
|
||
|
|
// 3. Role "viewer" AND minLevel
|
||
|
|
// is higher than "view" → 403 Forbidden (viewers are capped)
|
||
|
|
// 4. ACL lookup for (user, host) → 403 if not found or rank too low
|
||
|
|
func RequireHostAccess(store *db.Store, minLevel string) gin.HandlerFunc {
|
||
|
|
return func(c *gin.Context) {
|
||
|
|
claims := GetClaims(c)
|
||
|
|
if claims == nil {
|
||
|
|
c.AbortWithStatus(http.StatusUnauthorized)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Admins bypass all per-host ACL rules.
|
||
|
|
if claims.Role == "admin" {
|
||
|
|
c.Next()
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Viewers are hard-capped at the "view" level; reject any request
|
||
|
|
// that requires a higher privilege.
|
||
|
|
if claims.Role == "viewer" && LevelRank(minLevel) > LevelRank("view") {
|
||
|
|
c.AbortWithStatus(http.StatusForbidden)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Look up the requesting user.
|
||
|
|
user, err := store.GetUserByUsername(claims.Username)
|
||
|
|
if err != nil || user == nil {
|
||
|
|
c.AbortWithStatus(http.StatusForbidden)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check the host ACL entry.
|
||
|
|
hostIP := c.Param("ip")
|
||
|
|
level, ok, err := store.GetHostAccess(user.ID, hostIP)
|
||
|
|
if err != nil || !ok || LevelRank(level) < LevelRank(minLevel) {
|
||
|
|
c.AbortWithStatus(http.StatusForbidden)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
c.Next()
|
||
|
|
}
|
||
|
|
}
|