[#947] Vault tests, part 1 (#949)

This commit is contained in:
Michiel Borkent 2021-07-21 12:35:38 +02:00 committed by GitHub
parent 9c338c9b7f
commit 7fac0f1eb9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1153 additions and 43 deletions

View file

@ -86,7 +86,8 @@
io.replikativ/hasch {:mvn/version "0.3.7"}
com.grammarly/omniconf {:mvn/version "0.4.3"}
crispin/crispin {:mvn/version "0.3.8"}
org.clojure/data.json {:mvn/version "2.4.0"}}
org.clojure/data.json {:mvn/version "2.4.0"}
amperity/vault-clj {:mvn/version "1.0.4"}}
:classpath-overrides {org.clojure/clojure nil
org.clojure/spec.alpha nil
org.clojure/core.specs.alpha nil}}

View file

@ -150,6 +150,7 @@
java.lang.StringBuilder
java.lang.System
java.lang.Throwable
;; java.lang.UnsupportedOperationException
java.math.BigDecimal
java.math.BigInteger
java.math.MathContext
@ -206,6 +207,8 @@
java.security.SecureRandom
java.sql.Date
java.text.ParseException
;; adds about 200kb, same functionality provided by java.time:
;; java.text.SimpleDateFormat
~@(when features/java-time?
`[java.time.format.DateTimeFormatter
java.time.Clock
@ -396,6 +399,49 @@
(def class-map (gen-class-map))
(def imports
'{Appendable java.lang.Appendable
ArithmeticException java.lang.ArithmeticException
AssertionError java.lang.AssertionError
BigDecimal java.math.BigDecimal
BigInteger java.math.BigInteger
Boolean java.lang.Boolean
Byte java.lang.Byte
Character java.lang.Character
CharSequence java.lang.CharSequence
Class java.lang.Class
ClassNotFoundException java.lang.ClassNotFoundException
Comparable java.lang.Comparable
Double java.lang.Double
Exception java.lang.Exception
IndexOutOfBoundsException java.lang.IndexOutOfBoundsException
IllegalArgumentException java.lang.IllegalArgumentException
IllegalStateException java.lang.IllegalStateException
Integer java.lang.Integer
InterruptedException java.lang.InterruptedException
Iterable java.lang.Iterable
File java.io.File
Float java.lang.Float
Long java.lang.Long
Math java.lang.Math
NullPointerException java.lang.NullPointerException
Number java.lang.Number
NumberFormatException java.lang.NumberFormatException
Object java.lang.Object
Runtime java.lang.Runtime
RuntimeException java.lang.RuntimeException
Process java.lang.Process
ProcessBuilder java.lang.ProcessBuilder
Short java.lang.Short
StackTraceElement java.lang.StackTraceElement
String java.lang.String
StringBuilder java.lang.StringBuilder
System java.lang.System
Thread java.lang.Thread
Throwable java.lang.Throwable
;; UnsupportedOperationException java.lang.UnsupportedOperationException
})
(defn reflection-file-entries []
(let [entries (vec (for [c (sort (:all classes))
:let [class-name (str c)]]

View file

@ -415,47 +415,6 @@ Use bb run --help to show this help output.
'selmer.validator
@(resolve 'babashka.impl.selmer/selmer-validator-namespace))))
(def imports
'{Appendable java.lang.Appendable
ArithmeticException java.lang.ArithmeticException
AssertionError java.lang.AssertionError
BigDecimal java.math.BigDecimal
BigInteger java.math.BigInteger
Boolean java.lang.Boolean
Byte java.lang.Byte
Character java.lang.Character
CharSequence java.lang.CharSequence
Class java.lang.Class
ClassNotFoundException java.lang.ClassNotFoundException
Comparable java.lang.Comparable
Double java.lang.Double
Exception java.lang.Exception
IndexOutOfBoundsException java.lang.IndexOutOfBoundsException
IllegalArgumentException java.lang.IllegalArgumentException
IllegalStateException java.lang.IllegalStateException
Integer java.lang.Integer
InterruptedException java.lang.InterruptedException
Iterable java.lang.Iterable
File java.io.File
Float java.lang.Float
Long java.lang.Long
Math java.lang.Math
NullPointerException java.lang.NullPointerException
Number java.lang.Number
NumberFormatException java.lang.NumberFormatException
Object java.lang.Object
Runtime java.lang.Runtime
RuntimeException java.lang.RuntimeException
Process java.lang.Process
ProcessBuilder java.lang.ProcessBuilder
Short java.lang.Short
StackTraceElement java.lang.StackTraceElement
String java.lang.String
StringBuilder java.lang.StringBuilder
System java.lang.System
Thread java.lang.Thread
Throwable java.lang.Throwable})
(def edn-readers (cond-> {}
features/yaml?
(assoc 'ordered/map @(resolve 'flatland.ordered.map/ordered-map))))
@ -746,7 +705,7 @@ Use bb run --help to show this help output.
:env env
:features #{:bb :clj}
:classes classes/class-map
:imports imports
:imports classes/imports
:load-fn load-fn
:uberscript uberscript
:readers core/data-readers

View file

@ -237,6 +237,17 @@
(test-namespaces 'clojure.data.json-test
'clojure.data.json-test-suite-test)
(test-namespaces
;; TODO: env tests don't work because envoy lib isn't compatible with bb
#_'vault.env-test
'vault.lease-test
'vault.client.http-test
;; TODO:
;; failing tests in the following namespaces:
#_'vault.client.mock-test
#_'vault.secrets.kvv1-test
#_'vault.secrets.kvv2-test)
;;;; final exit code
(let [{:keys [:test :fail :error] :as m} @status]

View file

@ -0,0 +1,198 @@
(ns vault.client.http-test
(:require
[clojure.test :refer [deftest testing is]]
[vault.authenticate :as authenticate]
[vault.client.api-util :as api-util]
[vault.client.http :refer [http-client]]
[vault.core :as vault]
[vault.secrets.kvv1 :as vault-kvv1]))
(def example-url "https://vault.example.com")
(deftest http-client-instantiation
(is (thrown? IllegalArgumentException
(http-client nil)))
(is (thrown? IllegalArgumentException
(http-client :foo)))
(is (instance? vault.client.http.HTTPClient (http-client example-url))))
(deftest http-read-checks
(let [client (http-client example-url)]
(is (thrown? IllegalArgumentException
(vault-kvv1/read-secret client nil))
"should throw an exception on non-string path")
(is (thrown? RuntimeException
(vault-kvv1/read-secret client "secret/foo/bar"))
"should throw an exception on unauthenticated client")))
(deftest app-role
(let [api-endpoint (str example-url "/v1/auth/approle/login")
client (http-client example-url)
connection-attempt (atom nil)]
(with-redefs [api-util/do-api-request (fn [_method url _req]
(reset! connection-attempt url))
authenticate/api-auth! (constantly nil)]
(vault/authenticate! client :app-role {:secret-id "secret"
:role-id "role-id"})
(is (= @connection-attempt api-endpoint)
(str "should attempt to auth with: " api-endpoint)))))
(deftest authenticate-via-k8s
(testing "When a token file is available"
(let [client (http-client example-url)
api-requests (atom [])
api-auths (atom [])]
(with-redefs [api-util/do-api-request (fn [& args]
(swap! api-requests conj args)
:do-api-request-response)
authenticate/api-auth! (fn [& args]
(swap! api-auths conj args)
:api-auth!-response)]
(vault/authenticate! client :k8s {:jwt "fake-jwt-goes-here"
:role "my-role"})
(is (= [[:post
(str example-url "/v1/auth/kubernetes/login")
{:form-params {:jwt "fake-jwt-goes-here" :role "my-role"}
:content-type :json
:accept :json}]]
@api-requests))
(is (= [[(str "Kubernetes auth role=my-role")
(:auth client)
:do-api-request-response]]
@api-auths)))))
(testing "When no jwt is specified"
(let [client (http-client example-url)
api-requests (atom [])
api-auths (atom [])]
(with-redefs [api-util/do-api-request (fn [& args]
(swap! api-requests conj args))
authenticate/api-auth! (fn [& args]
(swap! api-auths conj args))]
(is (thrown? IllegalArgumentException
(vault/authenticate! client :k8s {:role "my-role"})))
(is (empty? @api-requests))
(is (empty? @api-auths)))))
(testing "When no role is specified"
(let [client (http-client example-url)
api-requests (atom [])
api-auths (atom [])]
(with-redefs [api-util/do-api-request (fn [& args]
(swap! api-requests conj args))
authenticate/api-auth! (fn [& args]
(swap! api-auths conj args))]
(is (thrown? IllegalArgumentException
(vault/authenticate! client :k8s {:jwt "fake-jwt-goes-here"})))
(is (empty? @api-requests))
(is (empty? @api-auths))))))
(deftest authenticate-via-aws
(testing "When all parameters are specified"
(let [client (http-client example-url)
api-requests (atom [])
api-auths (atom [])]
(with-redefs [api-util/do-api-request (fn [& args]
(swap! api-requests conj args)
:do-api-request-response)
authenticate/api-auth! (fn [& args]
(swap! api-auths conj args)
:api-auth!-response)]
(vault/authenticate! client :aws-iam {:role "my-role"
:http-request-method "POST"
:request-url "fake.sts.com"
:request-body "FakeAction&Version=1"
:request-headers "{'foo':'bar'}"})
(is (= [[:post
(str example-url "/v1/auth/aws/login")
{:form-params {:iam_http_request_method "POST"
:iam_request_url "fake.sts.com"
:iam_request_body "FakeAction&Version=1"
:iam_request_headers "{'foo':'bar'}"
:role "my-role"}
:content-type :json
:accept :json}]]
@api-requests))
(is (= [["AWS auth role=my-role"
(:auth client)
:do-api-request-response]]
@api-auths)))))
(testing "When no http-request-method is specified"
(let [client (http-client example-url)
api-requests (atom [])
api-auths (atom [])]
(with-redefs [api-util/do-api-request (fn [& args]
(swap! api-requests conj args))
authenticate/api-auth! (fn [& args]
(swap! api-auths conj args))]
(is (thrown? IllegalArgumentException
(vault/authenticate! client :aws-iam {:role "my-role"
:request-url "fake.sts.com"
:request-body "FakeAction&Version=1"
:request-headers "{'foo':'bar'}"})))
(is (empty? @api-requests))
(is (empty? @api-auths)))))
(testing "When no request-url is specified"
(let [client (http-client example-url)
api-requests (atom [])
api-auths (atom [])]
(with-redefs [api-util/do-api-request (fn [& args]
(swap! api-requests conj args))
authenticate/api-auth! (fn [& args]
(swap! api-auths conj args))]
(is (thrown? IllegalArgumentException
(vault/authenticate! client :aws-iam {:role "my-role"
:http-request-method "POST"
:request-body "FakeAction&Version=1"
:request-headers "{'foo':'bar'}"})))
(is (empty? @api-requests))
(is (empty? @api-auths)))))
(testing "When no request-body is specified"
(let [client (http-client example-url)
api-requests (atom [])
api-auths (atom [])]
(with-redefs [api-util/do-api-request (fn [& args]
(swap! api-requests conj args))
authenticate/api-auth! (fn [& args]
(swap! api-auths conj args))]
(is (thrown? IllegalArgumentException
(vault/authenticate! client :aws-iam {:role "my-role"
:http-request-method "POST"
:request-url "fake.sts.com"
:request-headers "{'foo':'bar'}"})))
(is (empty? @api-requests))
(is (empty? @api-auths)))))
(testing "When no request-headers is specified"
(let [client (http-client example-url)
api-requests (atom [])
api-auths (atom [])]
(with-redefs [api-util/do-api-request (fn [& args]
(swap! api-requests conj args))
authenticate/api-auth! (fn [& args]
(swap! api-auths conj args))]
(is (thrown? IllegalArgumentException
(vault/authenticate! client :aws-iam {:role "my-role"
:http-request-method "POST"
:request-url "fake.sts.com"
:request-body "FakeAction&Version=1"})))
(is (empty? @api-requests))
(is (empty? @api-auths)))))
(testing "When no role is specified"
(let [client (http-client example-url)
api-requests (atom [])
api-auths (atom [])]
(with-redefs [api-util/do-api-request (fn [& args]
(swap! api-requests conj args))
authenticate/api-auth! (fn [& args]
(swap! api-auths conj args))]
(is (thrown? IllegalArgumentException
(vault/authenticate! client :aws-iam {:http-request-method "POST"
:request-url "fake.sts.com"
:request-body "FakeAction&Version=1"
:request-headers "{'foo':'bar'}"})))
(is (empty? @api-requests))
(is (empty? @api-auths))))))

View file

@ -0,0 +1,74 @@
(ns vault.client.mock-test
(:require
[clojure.string :as str]
[clojure.test :refer [deftest testing is]]
[vault.core :as vault])
(:import
(clojure.lang
ExceptionInfo)))
(defn mock-client-authenticated
"A mock vault client using the secrets found in the given path, defaults to `vault/client/secret-fixture-logical.edn`"
([path]
(let [client (vault/new-client (str "mock:" path))]
(vault/authenticate! client :token "fake-token")
client))
([]
(mock-client-authenticated "vault/client/secret-fixture-logical.edn")))
(deftest create-token!-test
(testing "The return value of create-token is correct when not wrapped"
(let [result (vault/create-token! (mock-client-authenticated) {:no-default-policy true})]
(is (= ["root"] (:policies result)))
(is (= false (:renewable result)))
(is (= "" (:entity-id result)))
(is (= ["root"] (:token-policies result)))
(is (not (str/blank? (:accessor result))))
(is (= 0 (:lease-duration result)))
(is (= "service" (:token-type result)))
(is (= false (:orphan result)))
(is (not (str/blank? (:client-token result))))
(is (contains? result :metadata))))
(testing "The return value of create-token is correct when not wrapped and some options are specified"
(let [result (vault/create-token! (mock-client-authenticated) {:policies ["hello" "goodbye"]
:ttl "7d"})]
(is (= ["default" "hello" "goodbye"] (:policies result)))
(is (= false (:renewable result)))
(is (= "" (:entity-id result)))
(is (= ["default" "hello" "goodbye"] (:token-policies result)))
(is (not (str/blank? (:accessor result))))
(is (= 604800 (:lease-duration result)))
(is (= "service" (:token-type result)))
(is (= false (:orphan result)))
(is (not (str/blank? (:client-token result))))
(is (contains? result :metadata))))
(testing "The client throws a helpful error for debugging if ttl is incorrectly formatted"
(is (thrown-with-msg? ExceptionInfo
#"Mock Client doesn't recognize format of ttl"
(vault/create-token! (mock-client-authenticated) {:ttl "BLT"}))))
(testing "The return value of create-token is correct when not wrapped and some less common options are specified"
(let [result (vault/create-token! (mock-client-authenticated) {:policies ["hello" "goodbye"]
:ttl "10s"
:no-parent true
:no-default-policy true
:renewable true})]
(is (= ["hello" "goodbye"] (:policies result)))
(is (= true (:renewable result)))
(is (= "" (:entity-id result)))
(is (= ["hello" "goodbye"] (:token-policies result)))
(is (not (str/blank? (:accessor result))))
(is (= 10 (:lease-duration result)))
(is (= "service" (:token-type result)))
(is (= true (:orphan result)))
(is (not (str/blank? (:client-token result))))
(is (contains? result :metadata))))
(testing "The return value of create-token is correct when wrapped"
(let [result (vault/create-token! (mock-client-authenticated) {:wrap-ttl "2h"})]
(is (not (str/blank? (:token result))))
(is (not (str/blank? (:accessor result))))
(is (= 7200 (:ttl result)))
(is (not (str/blank? (:creation-time result))))
(is (= "auth/token/create" (:creation-path result)))
(is (not (str/blank? (:wrapped-accessor result)))))))

View file

@ -0,0 +1,2 @@
{"identities" {:batman "Bruce Wayne"
:captain-marvel "Carol Danvers"}}

View file

@ -0,0 +1,33 @@
(ns vault.env-test
(:require
[clojure.test :refer [deftest is]]
[vault.client.mock :refer [mock-client]]
[vault.env :as venv]))
(deftest uri-resolution
(let [client (mock-client {"some/path" {:id "foo"}})]
(is (thrown? Exception (venv/resolve-uri nil "vault:some/path#id"))
"resolution without client should throw")
(is (thrown? Exception (venv/resolve-uri client "vault:some/path#nope"))
"resoultion of nil secret should throw")
(is (= "foo" (venv/resolve-uri client "vault:some/path#id")))))
(deftest env-loading
(let [client (mock-client {"secret/foo" {:thing 123}
"secret/bar" {:id "abc"}})
env {:a "12345"
:b "vault:secret/foo#thing"
:c "vault:secret/bar#id"}]
(is (identical? env (venv/load! client env nil))
"resolution without whitelisted secrets returns env unchanged")
(is (= {:a "12345", :b 123, :c "abc"}
(venv/load! client env #{:a :b :c}))
"resolution allows direct passthrough")
(is (= {:a "12345", :b 123, :c "vault:secret/bar#id"}
(venv/load! client env #{:b}))
"resolution does not touch non-whitelisted vars")
(is (= {:a "12345", :b 123, :c "abc"}
(venv/load! client env #{:b :c :d}))
"resolution ignores missing whitelisted vars")))

View file

@ -0,0 +1,98 @@
(ns vault.lease-test
(:require
[clojure.test :refer [deftest is]]
[vault.lease :as lease])
(:import
java.time.Instant))
(defmacro with-time
"Evaluates the given body of forms with `vault.lease/now` rebound to always
give the result `t`."
[t & body]
`(with-redefs [vault.lease/now (constantly (Instant/ofEpochMilli ~t))]
~@body))
(deftest missing-info
(let [c (lease/new-store)]
(is (nil? (lease/lookup c :foo))
"lookup of unstored key should return nil")
(is (nil? (lease/update! c nil))
"storing nil should return nil")
(is (nil? (lease/lookup c :foo))
"lookup of nil store should return nil")))
(deftest secret-expiry
(let [c (lease/new-store)]
(with-time 1000
(is (= {:path "foo/bar"
:data {:bar "baz"}
:lease-id "foo/bar/12345"
:lease-duration 100
:renewable true
:vault.lease/issued (Instant/ofEpochMilli 1000)
:vault.lease/expiry (Instant/ofEpochMilli 101000)}
(lease/update! c {:path "foo/bar"
:lease-id "foo/bar/12345"
:lease-duration 100
:renewable true
:data {:bar "baz"}}))
"storing secret info should return data structure"))
(with-time 50000
(is (= {:path "foo/bar"
:data {:bar "baz"}
:lease-id "foo/bar/12345"
:lease-duration 100
:renewable true
:vault.lease/issued (Instant/ofEpochMilli 1000)
:vault.lease/expiry (Instant/ofEpochMilli 101000)}
(lease/lookup c "foo/bar"))
"lookup of stored secret within expiry should return data structure"))
(with-time 101001
(is (lease/expired? (lease/lookup c "foo/bar"))
"lookup of stored secret after expiry should return nil"))))
(deftest lease-filtering
(let [c (lease/new-store)
the-lease {:path "foo/bar"
:lease-id "foo/bar/12345"
:lease-duration 100
:renewable true
:vault.lease/renew true
:vault.lease/rotate true
:vault.lease/issued (Instant/ofEpochMilli 1000)
:vault.lease/expiry (Instant/ofEpochMilli 101000)}]
(with-time 1000
(lease/update! c {:path "foo/bar"
:data {:bar "baz"}
:lease-id "foo/bar/12345"
:lease-duration 100
:renewable true
:renew true
:rotate true}))
(with-time 101001
(is (= [the-lease] (lease/list-leases c))
"Basic lease listing should work, and the data should match.")
(is (= [the-lease] (lease/rotatable-leases c 0))
"Expired but rotatable lease should be considered rotatable"))
(with-time 100000
(is (= [the-lease] (lease/renewable-leases c 2)),
"Renewable leases should be listed when not expired yet.")
(is (empty? (lease/renewable-leases c 1)),
"Renewable leases should not be listed when outside the given window.")
(is (empty? (lease/rotatable-leases c 0))
"Non-expired, renewable leases should not be considered for rotation."))))
(deftest secret-invalidation
(let [c (lease/new-store)]
(is (some? (lease/update! c {:path "foo/bar"
:data {:baz "qux"}
:lease-id "foo/bar/12345"})))
(is (some? (lease/lookup c "foo/bar")))
(is (nil? (lease/remove-path! c "foo/bar")))
(is (nil? (lease/lookup c "foo/bar"))
"lookup of invalidated secret should return nil")))

View file

@ -0,0 +1,183 @@
(ns vault.secrets.kvv1-test
(:require
[cheshire.core :as json]
[clojure.test :refer [is testing deftest]]
[org.httpkit.client :as http]
[vault.client.http :as http-client]
[vault.client.mock-test :as mock-test]
[vault.core :as vault]
[vault.secrets.kvv1 :as vault-kvv1])
(:import
(clojure.lang
ExceptionInfo)))
;; -------- HTTP Client -------------------------------------------------------
(deftest list-secrets-test
(let [path "path/passed/in"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)
response {:auth nil
:data {:keys ["foo" "foo/"]}
:lease_duration 2764800
:lease-id ""
:renewable false}]
(vault/authenticate! client :token token-passed-in)
(testing "List secrets has correct response and sends correct request"
(with-redefs
[http/request
(fn [req]
(is (= :get (:method req)))
(is (= (str vault-url "/v1/" path) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (true? (-> req :query-params :list)))
(atom {:body response}))]
(is (= ["foo" "foo/"]
(vault-kvv1/list-secrets client path)))))))
(deftest read-secret-test
(let [lookup-response-valid-path (json/generate-string {:auth nil
:data {:foo "bar"
:ttl "1h"}
:lease_duration 3600
:lease_id ""
:renewable false})
path-passed-in "path/passed/in"
path-passed-in2 "path/passed/in2"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)]
(vault/authenticate! client :token token-passed-in)
(testing "Read secrets sends correct request and responds correctly if secret is successfully located"
(with-redefs
[http/request
(fn [req]
(is (= :get (:method req)))
(is (= (str vault-url "/v1/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(atom {:body lookup-response-valid-path}))]
(is (= {:foo "bar" :ttl "1h"} (vault-kvv1/read-secret client path-passed-in)))))
(testing "Read secrets sends correct request and responds correctly if no secret is found"
(with-redefs
[http/request
(fn [req]
(is (= (str vault-url "/v1/" path-passed-in2) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(throw (ex-info "not found" {:error [] :status 404})))]
(try
(vault-kvv1/read-secret client path-passed-in2)
(catch ExceptionInfo e
(is (= {:errors nil
:status 404
:type :vault.client.api-util/api-error}
(ex-data e)))))))))
(deftest write-secret-test
(let [create-success (json/generate-string {:data {:created_time "2018-03-22T02:24:06.945319214Z"
:deletion_time ""
:destroyed false
:version 1}})
write-data {:foo "bar"
:zip "zap"}
path-passed-in "path/passed/in"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)]
(vault/authenticate! client :token token-passed-in)
(testing "Write secrets sends correct request and returns true upon success"
(with-redefs
[http/request
(fn [req]
(is (= :post (:method req)))
(is (= (str vault-url "/v1/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= write-data (:form-params req)))
(atom {:body create-success
:status 204}))]
(is (true? (vault-kvv1/write-secret! client path-passed-in write-data)))))
(testing "Write secrets sends correct request and returns false upon failure"
(with-redefs
[http/request
(fn [req]
(is (= :post (:method req)))
(is (= (str vault-url "/v1/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= write-data
(:form-params req)))
(atom {:errors []
:status 400}))]
(is (false? (vault-kvv1/write-secret! client path-passed-in write-data)))))))
(deftest delete-secret-test
(let [path-passed-in "path/passed/in"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)]
(vault/authenticate! client :token "fake-token")
(testing "Delete secret returns correctly upon success, and sends correct request"
(with-redefs
[http/request
(fn [req]
(is (= :delete (:method req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= (str vault-url "/v1/" path-passed-in) (:url req)))
(atom {:status 204}))]
(is (true? (vault/delete-secret! client path-passed-in)))))
(testing "Delete secret returns correctly upon failure, and sends correct request"
(with-redefs
[http/request
(fn [req]
(is (= :delete (:method req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= (str vault-url "/v1/" path-passed-in) (:url req)))
(atom {:status 404}))]
(is (false? (vault/delete-secret! client path-passed-in)))))))
;; -------- Mock Client -------------------------------------------------------
(deftest mock-client-test
(testing "Mock client can correctly read values it was initialized with"
(is (= {:batman "Bruce Wayne"
:captain-marvel "Carol Danvers"}
(vault-kvv1/read-secret (mock-test/mock-client-authenticated) "identities"))))
(testing "Mock client correctly responds with a 404 to non-existent paths"
(is (thrown-with-msg? ExceptionInfo #"No such secret: hello"
(vault-kvv1/read-secret (mock-test/mock-client-authenticated) "hello")))
(is (thrown-with-msg? ExceptionInfo #"No such secret: identities"
(vault-kvv1/read-secret (vault/new-client "mock:-") "identities"))))
(testing "Mock client can write/update and read data"
(let [client (mock-test/mock-client-authenticated)]
(is (thrown-with-msg? ExceptionInfo #"No such secret: hello"
(vault-kvv1/read-secret client "hello")))
(is (true? (vault-kvv1/write-secret! client "hello" {:and-i-say "goodbye"})))
(is (true? (vault-kvv1/write-secret! client "identities" {:intersect "Chuck"})))
(is (= {:and-i-say "goodbye"}
(vault-kvv1/read-secret client "hello")))
(is (= {:intersect "Chuck"}
(vault-kvv1/read-secret client "identities")))))
(testing "Mock client can list secrets"
(let [client (mock-test/mock-client-authenticated)]
(is (empty? (vault-kvv1/list-secrets client "hello")))
(is (true? (vault-kvv1/write-secret! client "hello" {:and-i-say "goodbye"})))
(is (true? (vault-kvv1/write-secret! client "identities" {:intersect "Chuck"})))
(is (= ["identities" "hello"] (into [] (vault-kvv1/list-secrets client ""))))
(is (= ["identities"] (into [] (vault-kvv1/list-secrets client "identities"))))))
(testing "Mock client can delete secrets"
(let [client (mock-test/mock-client-authenticated)]
(is (true? (vault-kvv1/write-secret! client "hello" {:and-i-say "goodbye"})))
(is (= {:and-i-say "goodbye"}
(vault-kvv1/read-secret client "hello")))
(is (= {:batman "Bruce Wayne"
:captain-marvel "Carol Danvers"}
(vault-kvv1/read-secret client "identities")))
;; delete them
(is (true? (vault-kvv1/delete-secret! client "hello")))
(is (true? (vault-kvv1/delete-secret! client "identities")))
(is (thrown? ExceptionInfo (vault-kvv1/read-secret client "hello")))
(is (thrown? ExceptionInfo (vault-kvv1/read-secret client "identities"))))))

View file

@ -0,0 +1,493 @@
(ns vault.secrets.kvv2-test
(:require
[cheshire.core :as json]
[clojure.test :refer [testing deftest is]]
[org.httpkit.client :as http]
[vault.client.api-util :as api-util]
[vault.client.http :as http-client]
[vault.client.mock-test :as mock-test]
[vault.core :as vault]
[vault.secrets.kvv2 :as vault-kvv2])
(:import
(clojure.lang
ExceptionInfo)))
(deftest list-secrets-test
(let [path "path/passed/in"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)
response {:auth nil
:data {:keys ["foo" "foo/"]}
:lease_duration 2764800
:lease_id ""
:renewable false}]
(vault/authenticate! client :token token-passed-in)
(testing "List secrets has correct response and sends correct request"
(with-redefs
[http/request
(fn [req]
(is (= :get (:method req)))
(is (= (str vault-url "/v1/listmount/metadata/" path) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (true? (-> req :query-params :list)))
(atom {:body response}))]
(is (= ["foo" "foo/"]
(vault-kvv2/list-secrets client "listmount" path)))))))
(deftest write-config!-test
(let [mount "mount"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)
new-config-kebab {:max-versions 5
:cas-required false
:delete-version-after "3h25m19s"}
new-config-snake {:max_versions 5
:cas_required false
:delete_version_after "3h25m19s"}]
(vault/authenticate! client :token token-passed-in)
(testing "Write config sends correct request and returns true on valid call"
(with-redefs
[http/request
(fn [req]
(is (= :post (:method req)))
(is (= (str vault-url "/v1/" mount "/config") (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= new-config-snake (:form-params req)))
(atom {:status 204}))]
(is (true? (vault-kvv2/write-config! client mount new-config-kebab)))))))
(deftest read-config-test
(let [config {:max-versions 5
:cas-required false
:delete-version-after "3h25m19s"}
body-str (json/generate-string {:data (api-util/snakeify-keys config)})
mount "mount"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)]
(vault/authenticate! client :token token-passed-in)
(testing "Read config sends correct request and returns the config with valid call"
(with-redefs
[http/request
(fn [req]
(is (= :get (:method req)))
(is (= (str vault-url "/v1/" mount "/config") (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(atom {:body body-str}))]
(is (= config (vault-kvv2/read-config client mount)))))))
(deftest read-test
(let [lookup-response-valid-path (json/generate-string {:data {:data {:foo "bar"}
:metadata {:created_time "2018-03-22T02:24:06.945319214Z"
:deletion_time ""
:destroyed false
:version 1}}})
mount "mount"
path-passed-in "path/passed/in"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)]
(vault/authenticate! client :token token-passed-in)
(testing "Read secrets sends correct request and responds correctly if secret is successfully located"
(with-redefs
[http/request
(fn [req]
(is (= :get (:method req)))
(is (= (str vault-url "/v1/" mount "/data/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(atom {:body lookup-response-valid-path}))]
(is (= {:foo "bar"} (vault-kvv2/read-secret client mount path-passed-in)))))
(testing "Read secrets sends correct request and responds correctly if secret with version is successfully located"
(with-redefs
[http/request
(fn [req]
(is (= :get (:method req)))
(is (= (str vault-url "/v1/" mount "/data/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= {"version" 3} (:query-params req)))
(atom {:body lookup-response-valid-path}))]
(is (= {:foo "bar"} (vault-kvv2/read-secret client mount path-passed-in {:version 3 :force-read true})))))
(testing "Read secrets sends correct request and responds correctly if no secret is found"
(with-redefs
[http/request
(fn [req]
(is (= :get (:method req)))
(is (= (str vault-url "/v1/" mount "/data/different/path") (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(throw (ex-info "not found" {:errors [] :status 404 :type :vault.client.api-util/api-error})))]
(try
(is (= {:default-val :is-here}
(vault-kvv2/read-secret
client
mount
"different/path"
{:not-found {:default-val :is-here}})))
(vault-kvv2/read-secret client mount "different/path")
(is false)
(catch ExceptionInfo e
(is (= {:errors nil
:status 404
:type ::api-util/api-error}
(ex-data e)))))))))
(deftest write!-test
(let [create-success {:data {:created_time "2018-03-22T02:24:06.945319214Z"
:deletion_time ""
:destroyed false
:version 1}}
write-data {:foo "bar"
:zip "zap"}
mount "mount"
path-passed-in "path/passed/in"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)]
(vault/authenticate! client :token token-passed-in)
(testing "Write secrets sends correct request and returns true upon success"
(with-redefs
[http/request
(fn [req]
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= :post (:method req)))
(if (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req))
(do (is (= {} (:form-params req)))
(atom {:errors []
:status 200}))
(do (is (= (str vault-url "/v1/" mount "/data/" path-passed-in) (:url req)))
(is (= {:data write-data}
(:form-params req)))
(atom {:body create-success
:status 200}))))]
(is (= (:data create-success) (vault-kvv2/write-secret! client mount path-passed-in write-data)))))
(testing "Write secrets sends correct request and returns false upon failure"
(with-redefs
[http/request
(fn [req]
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= :post (:method req)))
(if (= (str vault-url "/v1/" mount "/metadata/other-path") (:url req))
(do (is (= {} (:form-params req)))
(atom {:errors []
:status 200}))
(do (is (= (str vault-url "/v1/" mount "/data/other-path") (:url req)))
(is (= {:data write-data}
(:form-params req)))
(atom {:errors []
:status 500}))))]
(is (false? (vault-kvv2/write-secret! client mount "other-path" write-data)))))))
(deftest delete-test
(let [mount "mount"
path-passed-in "path/passed/in"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)]
(vault/authenticate! client :token token-passed-in)
(testing "delete secrets send correct request and returns true upon success when no versions passed in"
(with-redefs
[http/request
(fn [req]
(is (= :delete (:method req)))
(is (= (str vault-url "/v1/" mount "/data/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(atom {:status 204}))]
(is (true? (vault-kvv2/delete-secret! client mount path-passed-in))
(is (true? (vault-kvv2/delete-secret! client mount path-passed-in []))))
(testing "delete secrets send correct request and returns false upon failure when no versions passed in"
(with-redefs
[http/request
(fn [req]
(is (= :delete (:method req)))
(is (= (str vault-url "/v1/" mount "/data/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(atom {:status 404}))]
(is (false? (vault-kvv2/delete-secret! client mount path-passed-in)))))
(testing "delete secrets send correct request and returns true upon success when multiple versions passed in"
(with-redefs
[http/request
(fn [req]
(is (= :post (:method req)))
(is (= (str vault-url "/v1/" mount "/delete/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= {:versions [12 14 147]} (:form-params req)))
(atom {:status 204}))]
(is (true? (vault-kvv2/delete-secret! client mount path-passed-in [12 14 147])))))
(testing "delete secrets send correct request and returns false upon failure when multiple versions passed in"
(with-redefs
[http/request
(fn [req]
(is (= :post (:method req)))
(is (= (str vault-url "/v1/" mount "/delete/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= {:versions [123]} (:form-params req)))
(atom {:status 404}))]
(is (false? (vault-kvv2/delete-secret! client mount path-passed-in [123])))))))))
(deftest read-metadata-test
(let [data (json/generate-string {:data
{:created_time "2018-03-22T02:24:06.945319214Z"
:current_version 3
:max_versions 0
:oldest_version 0
:updated_time "2018-03-22T02:36:43.986212308Z"
:versions {:1 {:created_time "2018-03-22T02:24:06.945319214Z"
:deletion_time ""
:destroyed false}
:2 {:created_time "2018-03-22T02:36:33.954880664Z"
:deletion_time ""
:destroyed false}
:3 {:created_time "2018-03-22T02:36:43.986212308Z"
:deletion_time ""
:destroyed false}}}})
kebab-metadata (json/generate-string {:created-time "2018-03-22T02:24:06.945319214Z"
:current-version 3
:max-versions 0
:oldest-version 0
:updated-time "2018-03-22T02:36:43.986212308Z"
:versions {:1 {:created-time "2018-03-22T02:24:06.945319214Z"
:deletion-time ""
:destroyed false}
:2 {:created-time "2018-03-22T02:36:33.954880664Z"
:deletion-time ""
:destroyed false}
:3 {:created-time "2018-03-22T02:36:43.986212308Z"
:deletion-time ""
:destroyed false}}})
mount "mount"
path-passed-in "path/passed/in"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)]
(vault/authenticate! client :token token-passed-in)
(testing "Sends correct request and responds correctly upon success"
(with-redefs
[http/request
(fn [req]
(is (= :get (:method req)))
(is (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(atom {:body data
:status 200}))]
(is (= kebab-metadata (json/generate-string (vault-kvv2/read-metadata client mount path-passed-in))))))
(testing "Sends correct request and responds correctly when metadata not found"
(with-redefs
[http/request
(fn [req]
(is (= :get (:method req)))
(is (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(throw (ex-info "not found" {:errors [] :status 404 :type :vault.client.api-util/api-error})))]
(is (thrown? ExceptionInfo (vault-kvv2/read-metadata client mount path-passed-in {:force-read true})))
(is (= 3 (vault-kvv2/read-metadata client mount path-passed-in {:not-found 3
:force-read true})))))))
(deftest write-metadata-test
(let [payload {:max-versions 5,
:cas-required false,
:delete-version-after "3h25m19s"}
mount "mount"
path-passed-in "path/passed/in"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)]
(vault/authenticate! client :token token-passed-in)
(testing "Write metadata sends correct request and responds with true upon success"
(with-redefs
[http/request
(fn [req]
(is (= :post (:method req)))
(is (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= (api-util/snakeify-keys payload) (:form-params req)))
(atom {:status 204}))]
(is (true? (vault-kvv2/write-metadata! client mount path-passed-in payload)))))
(testing "Write metadata sends correct request and responds with false upon failure"
(with-redefs
[http/request
(fn [req]
(is (= :post (:method req)))
(is (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= (api-util/snakeify-keys payload) (:form-params req)))
(atom {:status 500}))]
(is (false? (vault-kvv2/write-metadata! client mount path-passed-in payload)))))))
(deftest delete-metadata-test
(let [mount "mount"
path-passed-in "path/passed/in"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)]
(vault/authenticate! client :token token-passed-in)
(testing "Sends correct request and responds correctly upon success"
(with-redefs
[http/request
(fn [req]
(is (= :delete (:method req)))
(is (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(atom {:status 204}))]
(is (true? (vault-kvv2/delete-metadata! client mount path-passed-in)))))
(testing "Sends correct request and responds correctly upon failure"
(with-redefs
[http/request
(fn [req]
(is (= :delete (:method req)))
(is (= (str vault-url "/v1/" mount "/metadata/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(atom {:status 500}))]
(is (false? (vault-kvv2/delete-metadata! client mount path-passed-in)))))))
(deftest destroy!-test
(let [mount "mount"
path-passed-in "path/passed/in"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)
versions [1 2]]
(vault/authenticate! client :token token-passed-in)
(testing "Destroy secrets sends correct request and returns true upon success"
(with-redefs
[http/request
(fn [req]
(is (= :post (:method req)))
(is (= (str vault-url "/v1/" mount "/destroy/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= {:versions versions}
(:form-params req)))
(atom {:status 204}))]
(is (true? (vault-kvv2/destroy-secret! client mount path-passed-in versions)))))
(testing "Destroy secrets sends correct request and returns false upon failure"
(with-redefs
[http/request
(fn [req]
(is (= :post (:method req)))
(is (= (str vault-url "/v1/" mount "/destroy/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= {:versions [1]}
(:form-params req)))
(atom {:status 500}))]
(is (false? (vault-kvv2/destroy-secret! client mount path-passed-in [1])))))))
(deftest undelete-secret!-test
(let [mount "mount"
path-passed-in "path/passed/in"
token-passed-in "fake-token"
vault-url "https://vault.example.amperity.com"
client (http-client/http-client vault-url)
versions [1 2]]
(vault/authenticate! client :token token-passed-in)
(testing "Undelete secrets sends correct request and returns true upon success"
(with-redefs
[http/request
(fn [req]
(is (= :post (:method req)))
(is (= (str vault-url "/v1/" mount "/undelete/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= {:versions versions}
(:form-params req)))
(atom {:status 204}))]
(is (true? (vault-kvv2/undelete-secret! client mount path-passed-in versions)))))
(testing "Undelete secrets sends correct request and returns false upon failure"
(with-redefs
[http/request
(fn [req]
(is (= :post (:method req)))
(is (= (str vault-url "/v1/" mount "/undelete/" path-passed-in) (:url req)))
(is (= token-passed-in (get (:headers req) "X-Vault-Token")))
(is (= {:versions [1]}
(:form-params req)))
(atom {:status 500}))]
(is (false? (vault-kvv2/undelete-secret! client mount path-passed-in [1])))))))
;; -------- Mock Client -------------------------------------------------------
(defn mock-client-kvv2
"Creates a mock client with the data in `vault/secrets/secret-fixture-kvv2.edn`"
[]
(mock-test/mock-client-authenticated "vault/secrets/secret-fixture-kvv2.edn"))
(deftest mock-client-test
(testing "Mock client can correctly read values it was initialized with"
(is (= {:batman "Bruce Wayne"
:captain-marvel "Carol Danvers"}
(vault-kvv2/read-secret (mock-client-kvv2) "mount" "identities"))))
(testing "Mock client correctly responds with a 404 to reading non-existent paths"
(is (thrown-with-msg? ExceptionInfo #"No such secret: mount/data/hello"
(vault-kvv2/read-secret (mock-client-kvv2) "mount" "hello")))
(is (thrown-with-msg? ExceptionInfo #"No such secret: mount/data/identities"
(vault-kvv2/read-secret (vault/new-client "mock:-") "mount" "identities"))))
(testing "Mock client can write/update and read data"
(let [client (mock-client-kvv2)]
(is (thrown-with-msg? ExceptionInfo #"No such secret: mount/data/hello"
(vault-kvv2/read-secret client "mount" "hello")))
(is (true? (vault-kvv2/write-secret! client "mount" "hello" {:and-i-say "goodbye"})))
(is (true? (vault-kvv2/write-secret! client "mount" "identities" {:intersect "Chuck"})))
(is (= {:and-i-say "goodbye"}
(vault-kvv2/read-secret client "mount" "hello")))
(is (= {:intersect "Chuck"}
(vault-kvv2/read-secret client "mount" "identities")))))
(testing "Mock client can write and read config"
(let [client (mock-client-kvv2)
config {:max-versions 5
:cas-required false
:delete-version-after "3h23m19s"}]
(is (thrown? ExceptionInfo
(vault-kvv2/read-config client "mount")))
(is (true? (vault-kvv2/write-config! client "mount" config)))
(is (= config (vault-kvv2/read-config client "mount")))))
(testing "Mock client can write and read metadata"
(let [client (mock-client-kvv2)]
(is (thrown? ExceptionInfo
(vault-kvv2/read-metadata client "mount" "doesn't exist" {:force-read true})))
(is (= {:created-time "2018-03-22T02:24:06.945319214Z"
:current-version 1
:max-versions 0
:oldest-version 0
:updated-time "2018-03-22T02:36:43.986212308Z"
:versions {:1 {:created-time "2018-03-22T02:24:06.945319214Z"
:deletion-time ""
:destroyed false}}}
(vault-kvv2/read-metadata client "mount" "identities" {:force-read true})))
(is (true? (vault-kvv2/delete-metadata! client "mount" "identities")))
(is (thrown? ExceptionInfo
(vault-kvv2/read-metadata client "mount" "identities" {:force-read true})))
(is (true? (vault-kvv2/write-metadata! client "mount" "hello" {:max-versions 3})))
(is (= 3 (:max-versions (vault-kvv2/read-metadata client "mount" "hello"))))
(is (= 5 (vault-kvv2/read-metadata client "mount" "doesn't exist" {:force-read true
:not-found 5})))))
(testing "Mock client returns true if path is found on delete for secret, false if not when no versions specified"
(let [client (mock-client-kvv2)]
(is (true? (vault-kvv2/delete-secret! client "mount" "identities")))
(is (false? (vault-kvv2/delete-secret! client "mount" "eggsactly")))))
(testing "Mock client always returns true on delete for secret when versions specified"
(let [client (mock-client-kvv2)]
(is (true? (vault-kvv2/delete-secret! client "mount" "identities" [1])))
(is (true? (vault-kvv2/delete-secret! client "mount" "eggsactly" [4 5 6])))))
(testing "Mock can list secrets from their associated metadata"
(let [client (mock-client-kvv2)]
(is (empty? (vault-kvv2/list-secrets client "hello" "yes")))
(is (true? (vault-kvv2/write-secret! client "mount" "hello" {:and-i-say "goodbye"})))
;; Paths are good enough for mock right now, but be aware they are current
(is (= ["identities" "hello"]
(into [] (vault-kvv2/list-secrets client "mount" ""))))))
(testing "Mock client does not crash upon destroy"
(is (true? (vault-kvv2/destroy-secret! (mock-client-kvv2) "mount" "identities" [1]))))
(testing "Mock client does not crash upon undelete"
(is (true? (vault-kvv2/undelete-secret! (mock-client-kvv2) "mount" "identities" [1])))))

View file

@ -0,0 +1,12 @@
{"mount/data/identities"
{:data {:batman "Bruce Wayne"
:captain-marvel "Carol Danvers"}}
"mount/metadata/identities"
{:created_time "2018-03-22T02:24:06.945319214Z"
:current_version 1
:max_versions 0
:oldest_version 0
:updated_time "2018-03-22T02:36:43.986212308Z"
:versions {:1 {:created_time "2018-03-22T02:24:06.945319214Z"
:deletion_time ""
:destroyed false}}}}