Files
pocket-id/backend/internal/authz/router.go
T

103 lines
3.6 KiB
Go

package authz
import (
"fmt"
"net/http"
"path"
"strings"
"github.com/gin-gonic/gin"
)
// Router registers routes together with the scope each one requires
// Every route under /api must be registered through a Router or PublicRouter, which a test over the complete route table checks with IsDeclared
type Router struct {
group *gin.RouterGroup
auth *Middleware
optional bool
}
// Group returns a router for routes below the relative path
func (r *Router) Group(relativePath string) *Router {
return &Router{group: r.group.Group(relativePath), auth: r.auth, optional: r.optional}
}
// Optional returns a router whose routes let requests without a usable credential through as anonymous
// Handlers on these routes must check PrincipalFrom before relying on a signed-in user
func (r *Router) Optional() *Router {
return &Router{group: r.group, auth: r.auth, optional: true}
}
// Public returns a router for routes that require no authentication at all
func (r *Router) Public() *PublicRouter {
return &PublicRouter{group: r.group, auth: r.auth}
}
func (r *Router) GET(relativePath string, scope Scope, handlers ...gin.HandlerFunc) {
r.Handle(http.MethodGet, relativePath, scope, handlers...)
}
func (r *Router) POST(relativePath string, scope Scope, handlers ...gin.HandlerFunc) {
r.Handle(http.MethodPost, relativePath, scope, handlers...)
}
func (r *Router) PUT(relativePath string, scope Scope, handlers ...gin.HandlerFunc) {
r.Handle(http.MethodPut, relativePath, scope, handlers...)
}
func (r *Router) PATCH(relativePath string, scope Scope, handlers ...gin.HandlerFunc) {
r.Handle(http.MethodPatch, relativePath, scope, handlers...)
}
func (r *Router) DELETE(relativePath string, scope Scope, handlers ...gin.HandlerFunc) {
r.Handle(http.MethodDelete, relativePath, scope, handlers...)
}
// Handle registers a route that requires the scope, running authorization before every other handler of the route
func (r *Router) Handle(method, relativePath string, scope Scope, handlers ...gin.HandlerFunc) {
// An unknown scope can never be granted, so it is a programming error just like a duplicate route in gin
if !scope.Known() {
panic(fmt.Sprintf("route %s %s requires unknown scope %q", method, joinPaths(r.group.BasePath(), relativePath), scope))
}
chain := make([]gin.HandlerFunc, 0, len(handlers)+1)
chain = append(chain, r.auth.require(scope, r.optional))
chain = append(chain, handlers...)
r.group.Handle(method, relativePath, chain...)
r.auth.declare(method, joinPaths(r.group.BasePath(), relativePath))
}
// PublicRouter registers routes that require no authentication
// Routing them through here keeps public access an explicit decision instead of a missing middleware
type PublicRouter struct {
group *gin.RouterGroup
auth *Middleware
}
func (r *PublicRouter) GET(relativePath string, handlers ...gin.HandlerFunc) {
r.Handle(http.MethodGet, relativePath, handlers...)
}
func (r *PublicRouter) POST(relativePath string, handlers ...gin.HandlerFunc) {
r.Handle(http.MethodPost, relativePath, handlers...)
}
// Handle registers a public route
func (r *PublicRouter) Handle(method, relativePath string, handlers ...gin.HandlerFunc) {
r.group.Handle(method, relativePath, handlers...)
r.auth.declare(method, joinPaths(r.group.BasePath(), relativePath))
}
// joinPaths mirrors how gin builds a route's absolute path so declared routes match gin's route table
func joinPaths(absolutePath, relativePath string) string {
if relativePath == "" {
return absolutePath
}
finalPath := path.Join(absolutePath, relativePath)
if strings.HasSuffix(relativePath, "/") && !strings.HasSuffix(finalPath, "/") {
return finalPath + "/"
}
return finalPath
}