Created
June 23, 2021 19:57
-
-
Save cch1/8ac1436ff9167476a6933dd7020f09c0 to your computer and use it in GitHub Desktop.
An example of a simple templating capability that heavily leverages built-in features of clojure.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns template | |
(:require [clojure.edn :as edn] | |
[clojure.string :as string])) | |
;; Tagged literals do not need to be limited to being represented by a single string. Here we represent a value with a tuple of `magic` and `value`. | |
(defn read-magic [[magic value]] | |
(case magic | |
:c (string/capitalize value) | |
:l (string/lower-case value) | |
:u (string/upper-case value))) | |
;; CH templates provided two "interpolation" mechanisms: EDN's tagged literals and arbitrary Clojure code execution. | |
;; 1. EDN tagged literals: by treating the template as an EDN data structure we can introduce a controlled set of tagged literals. | |
;; The set of tagged literals available is static at run time (introduced in the first arg to edn/read-string) but the reader | |
;; for each tagged literal can be as rich as we want -including closures (see tag `c). | |
;; 2. Code evaluation: *IFF* we control the template, we can ensure it is "safe" to evaluate as Clojure code. This opens up | |
;; a world of complexity that should be carefully wielded. A simple affordance might be to dynamically bind some | |
;; well-known local symbols like `tenant` or `user` to provide a substitution mechanism. But be careful where these values (the | |
;; RHS of the binding pairs) comes from: strings read directly from HTTP query params (and possibly manipulated to cast them) are | |
;; safe because they cannot become s-expressions. | |
(defn interpolate-template | |
"Interpolate the edn serialized data `tstring` and evaluate with bindings provided by the `bindings` map" | |
[tstring environment bindings] | |
(let [bindings (mapcat identity bindings) | |
readers {'magic read-magic 'c (fn [x] (str environment "-" x))} ; adding new tags is this easy | |
template (edn/read-string {:readers readers} tstring)] | |
(eval `(let [~@bindings] ~template)))) | |
(let [query-params {"tenant" "gra"} | |
bindings {'user 23 | |
'tenant (keyword (get query-params "tenant" "gri")) ; this kind of binding injection is safe | |
'z '(prn "Arbitrary code can sneak in as a binding value ... be careful where you source them!")} | |
tstring "[tenant {:x 25 :user user :magic #magic [:u \"value\"]} (do (prn \"This is execution of arbitrary code in the template!\") 72) #c \"saas\"]"] | |
(interpolate-template tstring "production" bindings)) |
Add-ons: eval
in a dynamic namespace with a curated set of requires/imports/defs.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I used "user" in my example but in the context of authorization that should be applied unconditionally by surrounding code and not conflated with the user-facing templating functionality.