Skip to content

Commit

Permalink
Refactored config secret file loading
Browse files Browse the repository at this point in the history
  • Loading branch information
mdemare committed Sep 17, 2024
1 parent f31d6a3 commit 762419d
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 80 deletions.
91 changes: 22 additions & 69 deletions src/nl/surf/eduhub/validator/service/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -19,96 +19,49 @@
(ns nl.surf.eduhub.validator.service.config
(:require [clojure.java.io :as io]
[clojure.string :as str]
[environ.core :refer [env]]
[nl.jomco.envopts :as envopts]))

(def opt-specs
{:gateway-url ["URL of gateway" :str
:in [:gateway-url]]
:gateway-basic-auth-user ["Basic auth username of gateway" :str
:default nil
:in [:gateway-basic-auth :user]]
:gateway-basic-auth-pass ["Basic auth password of gateway" :str
:default nil
:in [:gateway-basic-auth :pass]]
:gateway-basic-auth-user-file ["Basic auth username of gateway, stored in secret file" :str
:default nil
:in [:gateway-basic-auth :user-file]]
:gateway-basic-auth-pass-file ["Basic auth password of gateway, stored in secret file" :str
:default nil
:in [:gateway-basic-auth :pass-file]]
:allowed-client-ids ["Comma separated list of allowed SurfCONEXT client ids." :str
:in [:allowed-client-ids]]
:surf-conext-client-id ["SurfCONEXT client id for validation service" :str
:default nil
:in [:introspection-basic-auth :user]]
:surf-conext-client-secret ["SurfCONEXT client secret for validation service" :str
:default nil
:in [:introspection-basic-auth :pass]]
:surf-conext-client-id-file ["SurfCONEXT client id for validation service, stored in secret file" :str
:default nil
:in [:introspection-basic-auth :user-file]]
:surf-conext-client-secret-file ["SurfCONEXT client secret for validation service, stored in secret file" :str
:default nil
:in [:introspection-basic-auth :pass-file]]
:surf-conext-introspection-endpoint ["SurfCONEXT introspection endpoint" :str
:in [:introspection-endpoint-url]]
:ooapi-version ["Ooapi version to pass through to gateway" :str
:in [:ooapi-version]]})

(def key-value-pairs-with-optional-secret-files
{:gateway-basic-auth-user [:gateway-basic-auth :user]
:gateway-basic-auth-pass [:gateway-basic-auth :pass]
:surf-conext-client-id [:introspection-basic-auth :user]
:surf-conext-client-secret [:introspection-basic-auth :pass]})
(defn- file-secret-loader-reducer [env-map value-key]
(let [file-key (keyword (str (name value-key) "-file"))
path (file-key env-map)]
(cond
(nil? path)
env-map

(defn- validate-required-secrets
" If a key \"K\" in `opt-specs` is not present, and a key \"K-file\"\n is present,
load the secret from that file and put it in the env map\n under K."
[config]
(let [missing-env (reduce
(fn [m [k v]] (if (get-in config v)
m
(assoc m k "missing")))
{}
key-value-pairs-with-optional-secret-files)]
(when (not-empty missing-env)
(.println *err* "Configuration error")
(.println *err* (envopts/errs-description missing-env))
(System/exit 1))
config))
(not (.exists (io/file path)))
(throw (ex-info (str "ENV var contains filename that does not exist: " path)
{:filename path, :env-path file-key}))

(defn dissoc-in
"Return nested map with path removed."
[m ks]
(let [path (butlast ks)
node (last ks)]
(if (empty? path)
(dissoc m node)
(update-in m path dissoc node))))
(value-key env-map)
(throw (ex-info "ENV var contains secret both as file and as value"
{:env-path [value-key file-key]}))

(defn- load-secret-from-file [config k]
(let [file-key-node (keyword (str (name (last k)) "-file")) ; The last entry in the :in array, with a "-file" suffix added
root-key-path (pop k) ; The :in array without the last item
file-key-path (conj root-key-path file-key-node) ; The :in array with the last item having a "-file" suffix
path (get-in config file-key-path) ; File path to secret
config (dissoc-in config file-key-path)] ; Remove -file key from config
(if (nil? path)
config
(if (.exists (io/file path))
(assoc-in config k (str/trim (slurp path))) ; Overwrite config with secret from file
(throw (ex-info (str "ENV var contains filename that does not exist: " path)
{:filename path, :env-path k}))))))
:else
(assoc env-map value-key (str/trim (slurp path))))))

(defn load-config-from-env []
(let [[config errs] (envopts/opts env opt-specs)]
(when errs
(.println *err* "Error in environment configuration")
(.println *err* (envopts/errs-description errs))
(.println *err* "Available environment vars:")
(.println *err* (envopts/specs-description opt-specs))
(System/exit 1))
(->> key-value-pairs-with-optional-secret-files
vals
(reduce load-secret-from-file config)
validate-required-secrets)))
;; These ENV keys may alternatively have a form in which the secret is contained in a file.
;; These ENV keys have a -file suffix, e.g.: gateway-basic-auth-pass-file
(def env-keys-with-alternate-file-secret
[:gateway-basic-auth-user :gateway-basic-auth-pass :surf-conext-client-id :surf-conext-client-secret])

(defn load-config-from-env [env-map]
(-> (reduce file-secret-loader-reducer env-map env-keys-with-alternate-file-secret)
(envopts/opts opt-specs)))
30 changes: 19 additions & 11 deletions src/nl/surf/eduhub/validator/service/main.clj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
[compojure.core :refer [defroutes GET]]
[compojure.route :as route]
[clojure.tools.logging :as log]
[environ.core :refer [env]]
[nl.jomco.envopts :as envopts]
[nl.jomco.http-status-codes :as http-status]
[nl.surf.eduhub.validator.service.authentication :as auth]
[nl.surf.eduhub.validator.service.config :as config]
Expand All @@ -31,7 +33,7 @@
[ring.middleware.json :refer [wrap-json-response]]))

(defn validate-endpoint [endpoint-id {:keys [gateway-url gateway-basic-auth ooapi-version] :as _config}]
(log/info "validating endpoint: " endpoint-id " - gateway-url: " gateway-url)
(log/info (str "validating endpoint: " endpoint-id " - gateway-url: " gateway-url))
(try
(let [response (http/get (str gateway-url "courses")
{:headers {"x-route" (str "endpoint=" endpoint-id)
Expand Down Expand Up @@ -71,13 +73,19 @@
server))

(defn -main [& _]
(let [config (config/load-config-from-env)
introspection-endpoint (:introspection-endpoint-url config)
introspection-auth (:introspection-basic-auth config)
allowed-client-id-set (set (str/split (:allowed-client-ids config) #","))]
(start-server (-> app-routes
(wrap-validator config)
(auth/wrap-allowed-clients-checker allowed-client-id-set)
(auth/wrap-authentication introspection-endpoint introspection-auth)
wrap-json-response
(wrap-defaults api-defaults)))))
(let [[config errs] (config/load-config-from-env env)]
(when errs
(.println *err* "Error in environment configuration")
(.println *err* (envopts/errs-description errs))
(.println *err* "Available environment vars:")
(.println *err* (envopts/specs-description config/opt-specs))
(System/exit 1))
(let [introspection-endpoint (:introspection-endpoint-url config)
introspection-auth (:introspection-basic-auth config)
allowed-client-id-set (set (str/split (:allowed-client-ids config) #","))]
(start-server (-> app-routes
(wrap-validator config)
(auth/wrap-allowed-clients-checker allowed-client-id-set)
(auth/wrap-authentication introspection-endpoint introspection-auth)
wrap-json-response
(wrap-defaults api-defaults))))))
71 changes: 71 additions & 0 deletions test/nl/surf/eduhub/validator/service/config_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
;; This file is part of eduhub-validator-service
;;
;; Copyright (C) 2022 SURFnet B.V.
;;
;; This program is free software: you can redistribute it and/or
;; modify it under the terms of the GNU Affero General Public License
;; as published by the Free Software Foundation, either version 3 of
;; the License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; Affero General Public License for more details.
;;
;; You should have received a copy of the GNU Affero General Public
;; License along with this program. If not, see
;; <https://www.gnu.org/licenses/>.

(ns nl.surf.eduhub.validator.service.config-test
(:require [clojure.test :refer [deftest is]]
[nl.surf.eduhub.validator.service.config :as config]
[nl.surf.eduhub.validator.service.main :as main])
(:import [clojure.lang ExceptionInfo]
[java.io File]))

(def app (main/wrap-validator main/app-routes {}))

(def default-env {:allowed-client-ids "default",
:gateway-basic-auth-pass "default",
:gateway-url "default",
:ooapi-version "default",
:surf-conext-client-id "default",
:surf-conext-client-secret "default",
:surf-conext-introspection-endpoint "default"})

(def default-expected-value {:allowed-client-ids "default",
:gateway-url "default",
:ooapi-version "default",
:gateway-basic-auth {:pass "default", :user "john200"},
:introspection-basic-auth {:pass "default", :user "default"},
:introspection-endpoint-url "default"})

(defn- test-env [env]
(config/load-config-from-env (merge default-env env)))

(deftest missing-secret
(is (= {:gateway-basic-auth-user "missing"}
(last (test-env {})))))

(deftest only-value-secret
(let [env {:gateway-basic-auth-user "john200"}]
(is (= [default-expected-value]
(test-env env)))))

(deftest only-file-secret
(let [path (.getAbsolutePath (File/createTempFile "test-secret" ".txt"))
env {:gateway-basic-auth-user-file path}]
(spit path "john200")
(is (= [default-expected-value]
(test-env env)))))

(deftest only-file-secret-file-missing
(let [env {:gateway-basic-auth-user-file "missing-file"}]
(is (thrown? ExceptionInfo (test-env env)))))

(deftest both-types-of-secret-specified
(let [path (.getAbsolutePath (File/createTempFile "test-secret" ".txt"))
env {:gateway-basic-auth-user "john200"
:gateway-basic-auth-user-file path}]
(spit path "john200")
(is (thrown? ExceptionInfo (test-env env)))))

0 comments on commit 762419d

Please sign in to comment.