-
Notifications
You must be signed in to change notification settings - Fork 12
/
rbac.clj
219 lines (195 loc) · 9.99 KB
/
rbac.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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
(ns puppetlabs.rbac-client.services.rbac
(:require
[clojure.string]
[clojure.tools.logging :as log]
[puppetlabs.i18n.core :as i18n]
[puppetlabs.http.client.common :as http]
[puppetlabs.http.client.sync :refer [create-client]]
[puppetlabs.rbac-client.core :refer [json-api-caller http-error?, parse-body]]
[puppetlabs.rbac-client.protocols.rbac :refer [RbacConsumerService]]
[puppetlabs.trapperkeeper.core :refer [defservice]]
[puppetlabs.trapperkeeper.services :refer [service-context]]
[slingshot.slingshot :refer [throw+]])
(:import [java.util UUID]
[java.net URI]))
(def ^:private ^:const authn-header "x-authentication")
(defn perm-str->map
"Given a permission string of the form <object_type>:<action>:(<instance>|*),
return a map that represents the permission.
COPY-PASTE: https://github.com/puppetlabs/pe-rbac-service/blob/master/src/clj/puppetlabs/rbac/services/consumer.clj"
[perm-str]
(let [[object-type action instance] (clojure.string/split perm-str #":" 3)]
{:object_type object-type
:action action
:instance instance}))
(defn str->uuid
"Convert a string into a UUID. If the object is already a UUID return it"
[str-uuid]
(try
(if (uuid? str-uuid) str-uuid (UUID/fromString str-uuid))
(catch IllegalArgumentException _
(throw+ {:uuid str-uuid
:msg (i18n/tru "Error parsing UUID {0}" str-uuid)
:kind ::invalid-uuid}))))
(defn group-ids-str->uuids
[subject-map]
(if-let [group-ids (:group_ids subject-map)]
(assoc subject-map :group_ids (map str->uuid group-ids))
subject-map))
(defn identity-provider-id-str->uuid
[subject-map]
(if-let [identity-provider-id (:identity_provider_id subject-map)]
(assoc subject-map :identity_provider_id (str->uuid identity-provider-id))
subject-map))
(defn parse-subject
[subject]
(when subject
(-> subject
(select-keys [:id :login :display_name :email
:last_login :role_ids :inherited_role_ids
:group_ids :is_superuser :is_revoked
:is_remote :is_group :identity_provider_id])
group-ids-str->uuids
identity-provider-id-str->uuid
(update :id str->uuid))))
(defn rbac-client
"Wrap the json caller adding :throw-body to opts"
([client rbac-url method path] (rbac-client client rbac-url method path {}))
([client rbac-url method path opts]
;; use merge to allow caller to override "throw-body"
(json-api-caller client rbac-url method path (merge {:throw-body true} opts))))
(defn api-url->status-url
"Given an RBAC api-url, convert that to the related status URL"
[^String api-url]
(let [^URI api-url (java.net.URI. api-url)]
(str (.getScheme api-url)
"://"
(.getHost api-url)
(when-not (= -1 (.getPort api-url))
(str ":" (.getPort api-url)))
"/status/v1/services")))
(defn request-certs-output
([prefix client ssl-client-cn]
(request-certs-output prefix client ssl-client-cn true))
([prefix client ssl-client-cn throw-error?]
(let [url (format "/%s/certs/%s" prefix ssl-client-cn)]
(-> (client :get url {:throw-body throw-error?})))))
(defn cert-allowed?
[context ssl-client-cn]
(when ssl-client-cn
(let [v1-certs? (deref (:v1-certs? context))
try-v2? (or (nil? v1-certs?) (false? v1-certs?))
{:keys [rbac-client]} context]
(if try-v2?
;; don't throw on failures to allow us to detect the 404
(let [v2-response (request-certs-output "v2" rbac-client ssl-client-cn false)
status (:status v2-response)]
(if (= 404 status)
(do
(swap! (:v1-certs? context) (constantly true))
(get-in (request-certs-output "v1" rbac-client ssl-client-cn)
[:body :whitelisted]))
;; if we got an error, make sure we handle it the way the rbac-client does
(if (http-error? v2-response)
(throw+ (update-in (parse-body v2-response) [:kind] keyword))
(get-in v2-response [:body :allowlisted]))))
(get-in (request-certs-output "v1" rbac-client ssl-client-cn)
[:body :whitelisted])))))
(defservice remote-rbac-consumer-service
RbacConsumerService
[[:ConfigService get-in-config]]
(init [_ tk-ctx]
(if-let [rbac-url (get-in-config [:rbac-consumer :api-url])]
(let [ssl-config (get-in-config [:global :certs])
authenticated-connection-limits {:max-connections-per-route (get-in-config [:rbac-consumer :max-connections-per-route-auth] 20)
:max-connections-total (get-in-config [:rbac-consumer :max-connections-total-auth] 20)}
unauthenticated-connection-limits {:max-connections-per-route (get-in-config [:rbac-consumer :max-connections-per-route-unauth] 20)
:max-connections-total (get-in-config [:rbac-consumer :max-connections-total-unauth] 20)}
certified-client (create-client (merge authenticated-connection-limits ssl-config))
uncertified-client (create-client (merge unauthenticated-connection-limits (select-keys ssl-config [:ssl-ca-cert])))]
(log/info (i18n/trs "Connection limit per route for authenticated clients has been set to {0} for the RBAC service." (:max-connections-per-route authenticated-connection-limits)))
(log/info (i18n/trs "Total connection limit for authenticated clients has been set to {0} for the RBAC service." (:max-connections-total authenticated-connection-limits)))
(log/info (i18n/trs "Connection limit per route for unauthenticated clients has been set to {0} for the RBAC service." (:max-connections-per-route unauthenticated-connection-limits)))
(log/info (i18n/trs "Total connection limit for unauthenticated clients has been set to {0} for the RBAC service." (:max-connections-total unauthenticated-connection-limits)))
(assoc tk-ctx
:client certified-client
:uncertified-client uncertified-client
:rbac-client (partial rbac-client certified-client rbac-url)
:uncertified-rbac-client (partial rbac-client uncertified-client rbac-url)
:status-client (partial rbac-client certified-client (api-url->status-url rbac-url))
:v1-certs? (atom nil)))
(throw+ {:kind :puppetlabs.rbac-client/invalid-configuration
:message (i18n/tru "''rbac-consumer'' not configured with an ''api-url''")})))
(stop [_ tk-ctx]
(when-let [client (:client tk-ctx)]
(http/close client))
(when-let [client (:uncertified-client tk-ctx)]
(http/close client))
(dissoc tk-ctx :client :uncertified-client))
(is-permitted? [this subject perm-str]
(let [{:keys [rbac-client]} (service-context this)
body {:token (str (:id subject))
:permissions [(perm-str->map perm-str)]}]
(-> (rbac-client :post "/v1/permitted" {:body body})
:body
first)))
(are-permitted? [this subject perm-strs]
(let [{:keys [rbac-client]} (service-context this)
body {:token (str (:id subject))
:permissions (map perm-str->map perm-strs)}]
(-> (rbac-client :post "/v1/permitted" {:body body})
:body)))
(cert-whitelisted? [this ssl-client-cn]
(log/warn (i18n/trs "DEPRECATION: the 'cert-whitelisted?' function has been deprecated and will be removed in future versions. Use `cert-allowed?` instead."))
(cert-allowed? (service-context this) ssl-client-cn))
(cert-allowed? [this ssl-client-cn]
(cert-allowed? (service-context this) ssl-client-cn))
(cert->subject [this ssl-client-cn]
(when ssl-client-cn
(let [context (service-context this)
v1-certs? (deref (:v1-certs? context))
try-v2? (or (nil? v1-certs?) (false? v1-certs?))
{:keys [rbac-client]} context]
(if try-v2?
;; don't throw on errors so we can detect the 404
(let [v2-response (request-certs-output "v2" rbac-client ssl-client-cn false)
status (:status v2-response)]
(if (= 404 status)
(do
(swap! (:v1-certs? context) (constantly true))
(-> (request-certs-output "v1" rbac-client ssl-client-cn)
(get-in [:body :subject])
parse-subject))
(if (http-error? v2-response)
(throw+ (update-in (parse-body v2-response) [:kind] keyword))
(parse-subject (get-in v2-response [:body :subject])))))
(parse-subject (get-in (request-certs-output "v1" rbac-client ssl-client-cn)
[:body :subject]))))))
(valid-token->subject [this token-str]
(let [{:keys [rbac-client]} (service-context this)
[with-suffix? actual-token _] (re-matches #"(.*)\|no_keepalive" token-str)
payload {:token (if with-suffix? actual-token token-str)
:update_last_activity? (not with-suffix?)}]
(-> (rbac-client :post "/v2/auth/token/authenticate" {:body payload})
:body
(parse-subject))))
(status [this level]
(let [{:keys [status-client]} (service-context this)]
(-> (status-client :get "" {:query-params {"level" (name level)}})
(get-in [:body :rbac-service])
(update :state keyword))))
(list-permitted [this token object-type action]
(let [{:keys [uncertified-rbac-client]} (service-context this)
headers {:headers {authn-header token}}]
(-> (uncertified-rbac-client :get (str "/v1/permitted/" object-type "/" action) headers)
:body)))
(list-permitted-for [this subject object-type action]
(let [{:keys [rbac-client]} (service-context this)
user-id (str (:id subject))]
(-> (rbac-client :get (str "/v1/permitted/" object-type "/" action "/" user-id))
:body)))
(subject [this user-id]
(let [{:keys [rbac-client]} (service-context this)]
(-> (rbac-client :get (str "/v1/users/" user-id))
:body
parse-subject))))