Browse Source

Masking variable values by default

Stephen Starkey 3 months ago
parent
commit
9b577da45c
6 changed files with 136 additions and 87 deletions
  1. 9
    1
      README.md
  2. 2
    2
      project.clj
  3. 17
    13
      spec/defenv/globals_spec.clj
  4. 34
    15
      spec/defenv/modular_spec.clj
  5. 46
    43
      src/defenv/core.clj
  6. 28
    13
      src/defenv/usage.clj

+ 9
- 1
README.md View File

@@ -10,6 +10,14 @@ easier.
10 10
 You'll need [Java 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)
11 11
 and [Leiningen](https://leiningen.org/).
12 12
 
13
+## Upgrading
14
+
15
+Going from `1.0.*` to `2.0.*`
16
+
17
+For security purposes, we have made masking of value display the default
18
+behavior. If you want to see the value of a variable, simply add 
19
+`:masked? false` to everything that you don't want to mask.
20
+
13 21
 ## Usage
14 22
 
15 23
 Say you want to define a binding in a namespace that is filled in by
@@ -46,7 +54,7 @@ This library was designed for <https://12factor.net/> apps
46 54
 
47 55
 ## License
48 56
 
49
-Copyright © 2019 Stephen Starkey
57
+Copyright © 2021 Stephen Starkey
50 58
 
51 59
     This program is free software: you can redistribute it and/or modify
52 60
     it under the terms of the GNU General Public License as published by

+ 2
- 2
project.clj View File

@@ -15,7 +15,7 @@
15 15
 ;     along with defenv.  If not, see <http://www.gnu.org/licenses/>.
16 16
 ;
17 17
 
18
-(defproject coreagile/defenv "1.0.11-SNAPSHOT"
18
+(defproject coreagile/defenv "2.0.0"
19 19
   :description "A library for managing environment variables in Clojure"
20 20
   :url "https://git.calmabiding.me/scstarkey/defenv"
21 21
   :license {:name "GNU General Public License v3"
@@ -28,7 +28,7 @@
28 28
   :pom-addition [:properties
29 29
                  [:maven.compiler.source "1.8"]
30 30
                  [:maven.compiler.target "1.8"]]
31
-  :profiles {:dev {:dependencies [[com.taoensso/timbre "4.10.0"]
31
+  :profiles {:dev {:dependencies [[com.taoensso/timbre "5.1.0"]
32 32
                                   [coreagile/specl-slingshot "0.0.3"]
33 33
                                   [hiccup/hiccup "1.0.5"]
34 34
                                   [org.clojure/tools.cli "1.0.194"]

+ 17
- 13
spec/defenv/globals_spec.clj View File

@@ -24,33 +24,37 @@
24 24
 (defn set-env []
25 25
   ;;Undefined vars
26 26
   (defenv log-level "Global log level." "LOG_LEVEL"
27
-    :tfn keyword :default "info")
27
+    :tfn keyword :default "info" :masked? false)
28 28
   (defenv should-log? "Should I log? A boolean." "SHOULD_LOG"
29
-    :tfn parse-bool :default "false")
30
-  (defenv optional "OPTIONAL" :tfn parse-int :optional? true)
31
-  (defenv bare "BARE")
29
+    :tfn parse-bool :default "false" :masked? false)
30
+  (defenv optional "OPTIONAL" :tfn parse-int :optional? true :masked? false)
31
+  (defenv bare "BARE" :masked? false)
32 32
 
33 33
   ;;Parse error
34 34
   (defenv unparseable-secret "Can't parse this secret." "UNPARSEABLE_SECRET"
35 35
     :tfn parse-int :default "secret" :masked? true)
36 36
   (defenv unparseable "Can't parse this." "UNPARSEABLE" :tfn parse-int
37
-    :default "broken")
37
+    :default "broken" :masked? false)
38 38
 
39 39
   ;; Testing we can remove the previous definition from the docstring
40 40
   (defenv thing "THING" :default "go away")
41
-  (defenv thing "THING" :masked? true)
41
+  (defenv thing "THING")
42 42
 
43 43
   (defenv amount "An amount. A double." "AMOUNT"
44
-    :tfn parse-double :default "1.5")
44
+    :tfn parse-double :default "1.5" :masked? false)
45 45
   (defenv acount "A count. An int." "COUNT"
46
-    :tfn parse-int :default "5")
47
-  (defenv another "This is really important!" "ANOTHER")
46
+    :tfn parse-int :default "5" :masked? false)
47
+  (defenv another "This is really important!" "ANOTHER"
48
+    :masked? false)
48 49
 
49 50
   ;;Vars defined in .env
50
-  (defenv fun "FUN" :masked? true)
51
-  (defenv apples "APPLES" :tfn parse-long :default "10")
52
-  (defenv weight "WEIGHT" :tfn parse-float)
53
-  (defenv eat? "Should we eat?" "EAT" :tfn parse-bool :default "false"))
51
+  (defenv fun "FUN")
52
+  (defenv apples "APPLES" :tfn parse-long :default "10" :masked? false)
53
+  (defenv weight "WEIGHT" :tfn parse-float :masked? false)
54
+  (defenv eat? "Should we eat?" "EAT"
55
+    :tfn parse-bool
56
+    :default "false"
57
+    :masked? false))
54 58
 
55 59
 (describe "global vars"
56 60
   (before-all (set-env)

+ 34
- 15
spec/defenv/modular_spec.clj View File

@@ -22,41 +22,60 @@
22 22
             [specl-slingshot.core :refer :all]
23 23
             [speclj.core :refer :all]))
24 24
 
25
-(def present-vars {:log-level {:env-name "LOG_LEVEL" :doc "Global log level."
26
-                               :tfn keyword :default "info"}
25
+(def present-vars {:log-level {:env-name "LOG_LEVEL"
26
+                               :doc "Global log level."
27
+                               :tfn keyword
28
+                               :default "info"
29
+                               :masked? false}
27 30
                    :should-log? {:env-name "SHOULD_LOG"
28 31
                                  :doc "Should I log? A boolean."
29
-                                 :tfn parse-bool :default "false"}
32
+                                 :tfn parse-bool
33
+                                 :default "false"
34
+                                 :masked? false}
30 35
                    :amount {:env-name "AMOUNT"
31 36
                             :doc "An amount. A double."
32
-                            :tfn parse-double :default "1.5"}
37
+                            :tfn parse-double
38
+                            :default "1.5"
39
+                            :masked? false}
33 40
                    :count {:env-name "COUNT"
34 41
                            :doc "A count. An int."
35
-                           :tfn parse-int :default "5"}
36
-                   :fun {:env-name "FUN" :masked? true}
37
-                   :apples {:env-name "APPLES" :tfn parse-long :default "10"}
38
-                   :weight {:env-name "WEIGHT" :tfn parse-float}
39
-                   :eat? {:env-name "EAT" :doc "Should we eat?"
40
-                          :tfn parse-bool :default "false"}
42
+                           :tfn parse-int
43
+                           :default "5"
44
+                           :masked? false}
45
+                   :fun {:env-name "FUN"}
46
+                   :apples {:env-name "APPLES"
47
+                            :tfn parse-long
48
+                            :default "10"
49
+                            :masked? false}
50
+                   :weight {:env-name "WEIGHT" :tfn parse-float :masked? false}
51
+                   :eat? {:env-name "EAT"
52
+                          :doc "Should we eat?"
53
+                          :tfn parse-bool
54
+                          :default "false"
55
+                          :masked? false}
41 56
                    :optional
42
-                   {:env-name "OPTIONAL" :tfn parse-int :optional? true}})
57
+                   {:env-name "OPTIONAL"
58
+                    :tfn parse-int
59
+                    :optional? true
60
+                    :masked? false}})
43 61
 
44 62
 (def missing-vars (assoc present-vars
45
-                    :thing {:env-name "THING" :masked? true}
63
+                    :thing {:env-name "THING"}
46 64
                     :another {:env-name "ANOTHER"
47 65
                               :doc "This is really important!"}
48 66
                     :unparseable {:env-name "UNPARSEABLE"
49 67
                                   :doc "Can't parse this."
50 68
                                   :default "broken"
51
-                                  :tfn parse-int}
69
+                                  :tfn parse-int
70
+                                  :masked? false}
52 71
 
53 72
                     ; Backwards compatibility
54 73
                     :bare {:env "BARE"}
55 74
 
56 75
                     :unparseable-secret {:env-name "UNPARSEABLE_SECRET"
57 76
                                          :doc "Can't parse this secret."
58
-                                         :default "secret" :tfn parse-int
59
-                                         :masked? true}))
77
+                                         :default "secret"
78
+                                         :tfn parse-int}))
60 79
 
61 80
 (describe "map parsing"
62 81
   (it "extracts values nicely"

+ 46
- 43
src/defenv/core.clj View File

@@ -69,7 +69,7 @@
69 69
    See `defenv.tfn-spec` for examples."
70 70
   ([s] (parse-map {} s))
71 71
   ([{:keys [default-key key-fn] :or {default-key "default"
72
-                                     key-fn identity}} s]
72
+                                     key-fn      identity}} s]
73 73
    (if (re-find #"=" s)
74 74
      (let [kvs (str/split s #",")]
75 75
        (reduce (fn [m s]
@@ -84,12 +84,12 @@
84 84
 ;; ## On-the-fly documentation
85 85
 
86 86
 ;; ### Generation
87
-(def ^:private displays {::missing "*REQUIRED*"
88
-                         ::masked "--MASKED--"
87
+(def ^:private displays {::missing     "*REQUIRED*"
88
+                         ::masked      "--MASKED--"
89 89
                          ::parse-error "*PARSE ERROR*"})
90 90
 
91 91
 (defn- get-var-status [{:keys [env-name v default masked? optional?]
92
-                        :as doc-map}]
92
+                        :as   doc-map}]
93 93
   (let [e-val (env env-name default)]
94 94
     (-> doc-map
95 95
         (assoc :value
@@ -179,13 +179,13 @@
179 179
   (let [has-param? (partial contains? params)
180 180
         env-args [env-name]
181 181
         base-value (env env-name default)]
182
-    {:tfn (if (and (not (nil? base-value))
183
-                   (has-param? :tfn)) tfn
184
-                                      identity)
185
-     :env-args (if (or optional? (has-param? :default))
186
-                 (conj env-args default) env-args)
182
+    {:tfn               (if (and (not (nil? base-value))
183
+                                 (has-param? :tfn)) tfn
184
+                                                    identity)
185
+     :env-args          (if (or optional? (has-param? :default))
186
+                          (conj env-args default) env-args)
187 187
      :params-to-display (assoc params :env-name env-name)
188
-     :base-value base-value}))
188
+     :base-value        base-value}))
189 189
 
190 190
 (defn- pretty-demunge
191 191
   [fn-object]
@@ -203,8 +203,11 @@
203 203
                         (if masked? "--ERROR MASKED--" (.getMessage e))))
204 204
          ::parse-error)))
205 205
 
206
-(defn- overlay-env [m k {:keys [optional? masked? env-name env] :as params}]
207
-  (let [env-name (or env-name env)
206
+(defn- overlay-env [m k raw-params]
207
+  (let [{:keys [optional? masked? env-name env] :as params}
208
+        (merge {:masked? true} raw-params)
209
+
210
+        env-name (or env-name env)
208 211
         {:keys [tfn params-to-display base-value]} (parse-env env-name params)
209 212
         v (try-tfn env-name masked? tfn base-value)]
210 213
     (-> m
@@ -265,8 +268,8 @@ by `defenv `that is required but missing."
265 268
   (when (empty? @global-parsed-env)
266 269
     (let [{:keys [env-map display-spec]} (get-env-info @global-defined-spec)]
267 270
       (dosync
268
-       (ref-set global-parsed-env env-map)
269
-       (ref-set global-display-spec display-spec))
271
+        (ref-set global-parsed-env env-map)
272
+        (ref-set global-display-spec display-spec))
270 273
       (doall (map #(% @global-display-spec) @on-load-handlers)))))
271 274
 
272 275
 (defn on-load! [f] (swap! on-load-handlers conj f))
@@ -281,9 +284,9 @@ by `defenv `that is required but missing."
281 284
   "Used primarily by `defenv` to retrieve the global environment state. "
282 285
   [env-name]
283 286
   (guarantee-global!
284
-   (if ((set (keys @global-parsed-env)) env-name)
285
-     (get @global-parsed-env env-name)
286
-     (throw-usage-if-missing @global-display-spec))))
287
+    (if ((set (keys @global-parsed-env)) env-name)
288
+      (get @global-parsed-env env-name)
289
+      (throw-usage-if-missing @global-display-spec))))
287 290
 
288 291
 (defn add-to-global-defined-spec!
289 292
   "Used primarily by `defenv` to add to the global environment state. "
@@ -325,8 +328,8 @@ missing."
325 328
                       remaining
326 329
                       (when env-or-fk (concat [env-or-fk] remaining)))
327 330
                     (apply hash-map)
328
-                    (add-doc doc-present? doc-or-env))
329
-        params (assoc params :env-name env-name)]
331
+                    (add-doc doc-present? doc-or-env)
332
+                    (merge {:env-name env-name}))]
330 333
     `(do (add-to-global-defined-spec! ~env-name ~params)
331 334
          (def ~(vary-meta b assoc :dynamic true)
332 335
            (delay (get-global-env ~env-name))))))
@@ -348,28 +351,28 @@ missing."
348 351
 
349 352
 ;; A recipe for doing your own display of environment information
350 353
 (comment
351
- (let [test-spec {:stuff {:env-name "STUFF" :default "bits" :doc "fun"}
352
-
353
-                  :answer
354
-                  {:env-name "ANSWER"
355
-                   :default "42"
356
-                   :tfn parse-int
357
-                   :doc "ultimate"}
358
-
359
-                  :path {:env-name "PATH"
360
-                         :tfn (fn [v] (str/split v #"[:;]"))
361
-                         :masked? true
362
-                         :doc "System path!"}}]
363
-
364
-   (println "\nInternal use:")
365
-   (->> test-spec env->map clojure.pprint/pprint)
366
-   (println "\nExternal view:")
367
-   (-> test-spec
368
-       display-spec
369
-       display-spec->docs
370
-       doc-table
371
-       println))
372
- )
354
+  (let [test-spec {:stuff {:env-name "STUFF" :default "bits" :doc "fun"}
355
+
356
+                   :answer
357
+                          {:env-name "ANSWER"
358
+                           :default  "42"
359
+                           :tfn      parse-int
360
+                           :doc      "ultimate"}
361
+
362
+                   :path  {:env-name "PATH"
363
+                           :tfn      (fn [v] (str/split v #"[:;]"))
364
+                           :masked?  true
365
+                           :doc      "System path!"}}]
366
+
367
+    (println "\nInternal use:")
368
+    (->> test-spec env->map clojure.pprint/pprint)
369
+    (println "\nExternal view:")
370
+    (-> test-spec
371
+        display-spec
372
+        display-spec->docs
373
+        doc-table
374
+        println))
375
+  )
373 376
 
374 377
 (defn display-env
375 378
   "Display the current environment to users in a friendly manner. If you call
@@ -399,5 +402,5 @@ missing."
399 402
 (defn reset-defined-env!
400 403
   []
401 404
   (with-new-parsed-env!
402
-   (alter global-display-spec empty)
403
-   (alter global-defined-spec empty)))
405
+    (alter global-display-spec empty)
406
+    (alter global-defined-spec empty)))

+ 28
- 13
src/defenv/usage.clj View File

@@ -33,25 +33,33 @@
33 33
 
34 34
 (env/defenv testing
35 35
   "An environment variable with a default value."
36
-  "DEFENV_TESTING" :default sensible-default)
36
+  "DEFENV_TESTING"
37
+  :default sensible-default
38
+  :masked? false)
37 39
 
38 40
 (env/defenv missing
39 41
   "Shows you what happens when something is missing."
40
-  "DEFENV_MISSING")
42
+  "DEFENV_MISSING"
43
+  :masked? false)
41 44
 
42
-(env/defenv missing-no-docs "DEFENV_MISSING_NO_DOCS")
45
+(env/defenv missing-no-docs "DEFENV_MISSING_NO_DOCS"
46
+  :masked? false)
43 47
 
44 48
 (env/defenv some-keyword
45 49
   "Shows how values can be converted to keywords."
46
-  "DEFENV_KEYWORD" :default "test" :tfn keyword)
50
+  "DEFENV_KEYWORD"
51
+  :default "test"
52
+  :tfn keyword
53
+  :masked? false)
47 54
 
48 55
 (env/defenv secret-thing
49
-  "Shows what happens when you mask a var."
50
-  "DEFENV_SECRET" :default "oops" :masked? true)
56
+  "Shows what happens when you don't unmask a var."
57
+  "DEFENV_SECRET"
58
+  :default "oops")
51 59
 
52 60
 (env/defenv truly-optional
53 61
   "A truly optional value."
54
-  "DEFENV_OPT" :tfn env/parse-int :optional? true)
62
+  "DEFENV_OPT" :tfn env/parse-int :optional? true :masked? false)
55 63
 
56 64
 (defn parse-broken [x]
57 65
   (throw (ExceptionInfo. (str "I refuse to parse: " x) {})))
@@ -62,19 +70,26 @@
62 70
   :tfn parse-broken :default "secret" :masked? true)
63 71
 
64 72
 (env/defenv parse-error "Something that can't be parsed." "DEFENV_UNPARSEABLE"
65
-  :tfn parse-broken :default "broken")
73
+  :tfn parse-broken :default "broken" :masked? false)
66 74
 
67 75
 ;; ### Local Map
68 76
 
69 77
 (def env-map-spec {:testing {:env-name "DEFENV_TESTING"
70
-                             :default sensible-default}
78
+                             :default sensible-default
79
+                             :masked? false}
71 80
                    :log-level {:env-name "LOG_LEVEL" :doc "Global log level."
72
-                               :tfn keyword :default "info"}
81
+                               :tfn keyword
82
+                               :default "info"
83
+                               :masked? false}
73 84
                    :should-log? {:env-name "SHOULD_LOG"
74 85
                                  :doc "Should I log? A boolean."
75
-                                 :tfn env/parse-bool :default "false"}
76
-                   :optional {:env-name "DEFENV_OPT" :optional? true
77
-                              :doc "A truly optional value."}})
86
+                                 :tfn env/parse-bool
87
+                                 :default "false"
88
+                                 :masked? false}
89
+                   :optional {:env-name "DEFENV_OPT"
90
+                              :optional? true
91
+                              :doc "A truly optional value."
92
+                              :masked? false}})
78 93
 (def env-map (env/env->map env-map-spec))
79 94
 
80 95
 (defmacro handle-exception [& body]