Composable handler->handler layers over Dream's primitives, with auth and permission POLICY injected so the layer is policy-free and testable: - middleware.sx: host/wrap-errors (JSON 500 via dream-catch-with), host/require-auth (bearer->principal via dream-bearer-token, JSON 401, injected token resolver), host/require-permission (lib/acl acl/permit? gate, JSON 403, injected resource extractor), host/pipeline (first = outermost) - feed.sx: POST /feed via host/feed-write-routes — auth ∘ ACL(post,feed) ∘ wrap-errors over host/feed-create (parse JSON body -> feed/post -> 201; non-object -> 400). Created activity reads back via GET /feed. - middleware suite (9) + feed write tests (6 new); conformance preloads now include the Datalog engine + ACL subsystem + Dream auth/error. ACL works with string atoms (no symbol coercion). Mute/prefs layer and sxtp.sx deferred to the next tick. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
2.8 KiB
Plaintext
55 lines
2.8 KiB
Plaintext
;; lib/host/middleware.sx — Host middleware: composable handler->handler layers
|
|
;; for the cross-cutting concerns every write endpoint shares — error trapping
|
|
;; (JSON 500), authentication (bearer token -> principal), and authorisation
|
|
;; (ACL permit?). Middleware is plain function composition; host/pipeline threads a
|
|
;; list onto a handler, FIRST middleware outermost (so it runs first). Auth and
|
|
;; permission policy are INJECTED — the token resolver and the resource extractor —
|
|
;; so this layer carries no hardcoded policy. Reuses Dream's bearer/error helpers
|
|
;; and lib/acl's public acl/permit?.
|
|
;; Depends on lib/dream/{auth,error,router}.sx + lib/acl/api.sx + lib/host/handler.sx.
|
|
|
|
;; Compose a list of middlewares onto a handler (first = outermost).
|
|
(define host/pipeline
|
|
(fn (middlewares handler)
|
|
(dr/apply-middlewares middlewares handler)))
|
|
|
|
;; The authenticated principal attached by host/require-auth.
|
|
(define host/principal (fn (req) (dream-principal req)))
|
|
|
|
;; ── error trapping ─────────────────────────────────────────────────
|
|
;; Any error thrown downstream becomes a JSON 500 envelope.
|
|
(define host/-on-error
|
|
(fn (req e) (host/error 500 "internal error")))
|
|
(define host/wrap-errors (dream-catch-with host/-on-error))
|
|
|
|
;; ── authentication ─────────────────────────────────────────────────
|
|
;; resolve : token -> principal | nil. Missing/invalid token -> JSON 401 with a
|
|
;; WWW-Authenticate: Bearer challenge; success attaches :dream-principal so
|
|
;; downstream layers (and host/principal) can read it.
|
|
(define host/require-auth
|
|
(fn (resolve)
|
|
(fn (next)
|
|
(fn (req)
|
|
(let ((tok (dream-bearer-token req)))
|
|
(let ((principal (if tok (resolve tok) nil)))
|
|
(if (nil? principal)
|
|
(dream-add-header
|
|
(host/error 401 "unauthorized")
|
|
"www-authenticate"
|
|
"Bearer")
|
|
(next (assoc req :dream-principal principal)))))))))
|
|
|
|
;; ── authorisation ──────────────────────────────────────────────────
|
|
;; Gate on ACL: the authed principal must be permitted `action` on the resource
|
|
;; computed by res-fn from the request. Denied -> JSON 403. Assumes the ACL fact
|
|
;; db was loaded (acl/load!) at startup. Place AFTER host/require-auth.
|
|
(define host/require-permission
|
|
(fn (action res-fn)
|
|
(fn (next)
|
|
(fn (req)
|
|
(let ((subject (host/principal req))
|
|
(resource (res-fn req)))
|
|
(if (acl/permit? subject action resource)
|
|
(next req)
|
|
(host/error 403 "forbidden")))))))
|