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() } }