/
api_util.clj
158 lines (138 loc) · 4.98 KB
/
api_util.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
(ns vault.client.api-util
(:require
[cheshire.core :as json]
[clj-http.client :as http]
[clojure.string :as str]
[clojure.tools.logging :as log]
[clojure.walk :as walk])
(:import
(clojure.lang
ExceptionInfo)
(java.security
MessageDigest)
(org.apache.commons.codec.binary
Hex)))
;; ## API Utilities
(defmacro supports-not-found
"Tries to perform the body, which likely includes an API call. If a `404` `::api-error` occurs, looks for and returns
the value of `:not-found` in `on-fail-opts` if present"
[on-fail-opts & body]
`(try
~@body
(catch ExceptionInfo ex#
(let [api-fail-options# ~on-fail-opts]
(if (and (contains? api-fail-options# :not-found)
(= ::api-error (:type (ex-data ex#)))
(= 404 (:status (ex-data ex#))))
(:not-found api-fail-options#)
(throw ex#))))))
(defn- ^:no-doc keyword-swap-chars
"Rewrites keyword map keys with underscores changed to dashes."
[value find replace]
(let [replace-kw #(-> % name (str/replace find replace) keyword)
xf-entry (juxt (comp replace-kw key) val)]
(walk/postwalk
(fn xf-maps
[x]
(if (map? x)
(into {} (map xf-entry) x)
x))
value)))
(defn ^:no-doc kebabify-keys
"Rewrites keyword map keys with underscores changed to dashes."
[value]
(keyword-swap-chars value "_" "-"))
(defn ^:no-doc snakeify-keys
"Rewrites keyword map keys with dashes changed to underscores."
[value]
(keyword-swap-chars value "-" "_"))
(defn ^:no-doc sha-256
"Geerate a SHA-2 256 bit digest from a string."
[s]
(let [hasher (MessageDigest/getInstance "SHA-256")
str-bytes (.getBytes (str s) "UTF-8")]
(.update hasher str-bytes)
(Hex/encodeHexString (.digest hasher))))
(defn ^:no-doc clean-body
"Cleans up a response from the Vault API by rewriting some keywords and
dropping extraneous information. Note that this changes the `:data` in the
response to the original result to preserve accuracy."
[response]
(->
(:body response)
(kebabify-keys)
(assoc :data (:data (:body response)))
(->> (into {} (filter (comp some? val))))))
(defn ^:no-doc api-error
"Inspects an exception and returns a cleaned-up version if the type is well
understood. Otherwise returns the original error."
[ex]
(let [data (ex-data ex)
status (:status data)]
(if (and status (<= 400 status))
(let [body (try
(json/parse-string (:body data) true)
(catch Exception _
nil))
errors (if (:errors body)
(str/join ", " (:errors body))
(pr-str body))]
(ex-info (str "Vault API errors: " errors)
{:type ::api-error
:status status
:errors (:errors body)}
ex))
ex)))
(defn ^:no-doc do-api-request
"Performs a request against the API, following redirects at most twice. The
`request-url` should be the full API endpoint."
[method request-url req]
(let [redirects (::redirects req 0)]
(when (<= 2 redirects)
(throw (ex-info (str "Aborting Vault API request after " redirects " redirects")
{:method method, :url request-url})))
(let [resp (try
(http/request (assoc req :method method :url request-url))
(catch Exception ex
(throw (api-error ex))))]
(if-let [location (and (#{303 307} (:status resp))
(get-in resp [:headers "Location"]))]
(do (log/debug "Retrying API request redirected to " location)
(recur method location (assoc req ::redirects (inc redirects))))
resp))))
(defn ^:no-doc api-request
"Helper method to perform an API request with common headers and values.
Currently always uses API version `v1`. The `path` should be relative to the
version root."
[client method path req]
; Check API path.
(when-not (and (string? path) (not (str/blank? path)))
(throw (IllegalArgumentException.
(str "API path must be a non-empty string, got: "
(pr-str path)))))
; Check client authentication.
(when-not (some-> client :auth deref :client-token)
(throw (IllegalStateException.
"Cannot call API path with unauthenticated client.")))
; Call API with standard arguments.
(do-api-request
method
(str (:api-url client) "/v1/" path)
(merge
(:http-opts client)
{:accept :json
:as :json}
req
{:headers (merge {"X-Vault-Token" (:client-token @(:auth client))}
(:headers req))})))
(defn ^:no-doc unwrap-secret
"Common function to call the token unwrap endpoint."
[client wrap-token]
(do-api-request
:post (str (:api-url client) "/v1/sys/wrapping/unwrap")
(merge
(:http-opts client)
{:headers {"X-Vault-Token" wrap-token}
:content-type :json
:accept :json
:as :json})))