CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / openapi (push) Successful in 27s
CI / web (push) Successful in 51s
CI / go (push) Successful in 2m19s
CI / bird2 (push) Successful in 13s
CI / release (push) Successful in 4m24s
Added support for portal JWT authentication, enabling single sign-on (SSO) capabilities. Updated the application to handle JWT claims for user permissions and roles, enhancing security and access control. Refactored relevant components and API routes to accommodate the new authentication flow, ensuring a seamless user experience. Updated documentation to reflect the new authentication requirements and configurations. Co-authored-by: Cursor <cursoragent@cursor.com>
108 lines
2.8 KiB
Go
108 lines
2.8 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// Portal permission strings. Format: <app>:<section>:<level> (bgp:modules:read).
|
|
// Superset order: admin ⊃ write ⊃ read for the same <app>:<section>.
|
|
const (
|
|
permLevelRead = "read"
|
|
permLevelWrite = "write"
|
|
permLevelAdmin = "admin"
|
|
)
|
|
|
|
// permLevelRank returns 0 for unknown, 1 for read, 2 for write, 3 for admin.
|
|
func permLevelRank(level string) int {
|
|
switch strings.ToLower(strings.TrimSpace(level)) {
|
|
case permLevelRead:
|
|
return 1
|
|
case permLevelWrite:
|
|
return 2
|
|
case permLevelAdmin:
|
|
return 3
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// splitPerm splits a permission string into (app, section, level).
|
|
func splitPerm(perm string) (app, section, level string, ok bool) {
|
|
parts := strings.Split(strings.TrimSpace(perm), ":")
|
|
if len(parts) != 3 {
|
|
return "", "", "", false
|
|
}
|
|
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), strings.TrimSpace(parts[2]), true
|
|
}
|
|
|
|
// HasPermission reports whether the granted list satisfies required, applying the
|
|
// admin ⊃ write ⊃ read superset within the same app+section.
|
|
func HasPermission(granted []string, required string) bool {
|
|
rApp, rSection, rLevel, ok := splitPerm(required)
|
|
if !ok {
|
|
return false
|
|
}
|
|
needRank := permLevelRank(rLevel)
|
|
if needRank == 0 {
|
|
return false
|
|
}
|
|
for _, g := range granted {
|
|
gApp, gSection, gLevel, ok := splitPerm(g)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if !strings.EqualFold(gApp, rApp) || !strings.EqualFold(gSection, rSection) {
|
|
continue
|
|
}
|
|
if permLevelRank(gLevel) >= needRank {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// permAPIKeyRoleFor maps a permission level to the API-key role required.
|
|
func permAPIKeyRoleFor(perm string) string {
|
|
_, _, level, ok := splitPerm(perm)
|
|
if !ok {
|
|
return "operator"
|
|
}
|
|
switch strings.ToLower(level) {
|
|
case permLevelRead:
|
|
return "viewer"
|
|
case permLevelWrite:
|
|
return "editor"
|
|
case permLevelAdmin:
|
|
return "operator"
|
|
default:
|
|
return "operator"
|
|
}
|
|
}
|
|
|
|
// requirePerm enforces a permission for a portal JWT or falls back to the API-key role ladder.
|
|
// node/firewall roles are always rejected (they use requireNode / requireFirewall).
|
|
func (s *Server) requirePerm(w http.ResponseWriter, a Auth, perm string) bool {
|
|
switch strings.ToLower(a.Role) {
|
|
case "node":
|
|
writeProblem(w, http.StatusForbidden, "Forbidden", "node role cannot access this resource")
|
|
return false
|
|
case "firewall":
|
|
writeProblem(w, http.StatusForbidden, "Forbidden", "firewall role cannot access this resource")
|
|
return false
|
|
}
|
|
if a.Kind == AuthKindJWT || len(a.Permissions) > 0 {
|
|
if a.IsAdmin || HasPermission(a.Permissions, perm) {
|
|
return true
|
|
}
|
|
writeProblem(w, http.StatusForbidden, "Forbidden", "missing permission: "+perm)
|
|
return false
|
|
}
|
|
need := permAPIKeyRoleFor(perm)
|
|
if roleLevel(a.Role) < roleLevel(need) {
|
|
writeProblem(w, http.StatusForbidden, "Forbidden", "insufficient role")
|
|
return false
|
|
}
|
|
return true
|
|
}
|