Skip to content

Commit

Permalink
CMR-9125 refactors user token lookup in ous to use EDL
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-zamora committed May 30, 2023
1 parent 04c25ab commit 90dfd26
Show file tree
Hide file tree
Showing 14 changed files with 153 additions and 40 deletions.
3 changes: 2 additions & 1 deletion other/cmr-exchange/authz/project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
:url "https://github.com/cmr-exchange/authz"
:license {:name "Apache License, Version 2.0"
:url "http://www.apache.org/licenses/LICENSE-2.0"}
:dependencies [[cheshire "5.8.1"]
:dependencies [[buddy/buddy-sign "3.4.333"]
[cheshire "5.8.1"]
[clojusc/trifl "0.4.2"]
[clojusc/twig "0.4.0"]
[com.stuartsierra/component "0.3.2"]
Expand Down
12 changes: 11 additions & 1 deletion other/cmr-exchange/authz/resources/config/cmr-authz/config.edn
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{:auth-caching
{:init {}
{:jwt-public-key "{
\"kty\": \"RSA\",
\"n\": \"3LopSyeoSZZGorSPjk4mMbR0ybVSLvfrONGSGXCNXE6ScX9Y1QC_zV8fVeh4XO8tYDi9CgzqK3Nhjsd5KI0ZzTI8Lf52tyr7OzebZXGZpMvyvdp59wlbPL4WFkIHvWFpgypSrTQIRENKaYW_yQB9srq6JpUx14aRG5TpiBuPqgnGM-qBqPvLq5LX9kVhqbV46TuZd9uPn_gISut7A7K3Y5S24DZd3ebxXPap1cn6-mIY30QG5oYmVlMZxdVPhnTzjj4ZNsfyKSRKq3F_UapEr4ynhr-ONgj8HyozyFqTpUn3o8pKAMVaOEfZmRlqb3jnQknbcsJ9fNxmWUUZr_PysQ\",
\"e\": \"AQAB\",
\"kid\": \"edljwtpubkey_sit\"
}"
:init {}
:ttl
{:minutes 60}
:lru
Expand All @@ -13,6 +19,10 @@
:relative
{:root
{:url "/access-control"}}}}
:edl
{:rest
{:protocol "https"
:host "sit.urs.earthdata.nasa.gov"}}
:echo
{:rest
{:protocol "https"
Expand Down
17 changes: 15 additions & 2 deletions other/cmr-exchange/authz/src/cmr/authz/components/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
;; The URLs returned by these functions have no trailing slash:
(def get-access-control-url #(get-service-url % :access-control))
(def get-echo-rest-url #(get-service-url % :echo-rest))
(def get-edl-rest-url #(get-service-url % :edl-rest))

(defn cache-dumpfile
[system]
Expand All @@ -45,13 +46,25 @@
[system]
(* (get-in (get-cfg system) [:auth-caching :ttl :minutes]) ; minutes
60 ; seconds
1000 ; milliseconds
))
1000)) ; milliseconds

(defn cache-type
[system]
(get-in (get-cfg system) [:auth-caching :type]))

(defn get-jwt-public-key
[system]
(get-in (get-cfg system) [:auth-caching :jwt-public-key]))

(defn get-edl-username
[system]
(get-in (get-cfg system) [:cmr :edl :username]))

(defn get-edl-password
[system]
(get-in (get-cfg system) [:cmr :edl :password]))


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; Component Lifecycle Implementation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down
6 changes: 5 additions & 1 deletion other/cmr-exchange/authz/src/cmr/authz/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@

(or (= service :echo)
(= service :echo-rest))
[:echo :rest]))
[:echo :rest]

(or (= service :edl)
(= service :edl-rest))
[:edl :rest]))

(defn service->base-url
[service]
Expand Down
2 changes: 1 addition & 1 deletion other/cmr-exchange/authz/src/cmr/authz/errors.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

(def error-code 403)
(def no-permissions "You do not have permissions to access that resource.")
(def token-required "An ECHO token is required to access this resource.")
(def token-required "A valid token is required to access this resource.")

(def status-map
"This is a lookup data structure for how HTTP status/error codes map to CMR
Expand Down
95 changes: 77 additions & 18 deletions other/cmr-exchange/authz/src/cmr/authz/token.clj
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,26 @@
to CMR Access Control to get token-to-user mappings, extracting tokens from
request headers, and defining caching keys and related tasks."
(:require
[buddy.core.keys :as buddy-keys]
[buddy.sign.jwt :as jwt]
[cheshire.core :as json]
[clojure.data.xml :as xml]
[clojure.string :as string]
[cmr.authz.components.config :as config]
[cmr.authz.http :as http]
[cmr.http.kit.request :as request]
[cmr.http.kit.response :as response]
[taoensso.timbre :as log]
[xml-in.core :as xml-in]))


(def token-info-resource
"The path segment to the ECHO REST API resource that is queried
in order to get user/token mappings."
"/tokens/get_token_info")
"EDL Launchpad token verification resource."
"/api/nams/edl_user")

(def client-token-resource
"EDL client bearer token resource."
"/oauth/token?grant_type=client_credentials")

(defn token-data-key
"Generate a key to be used for caching token data."
Expand Down Expand Up @@ -50,6 +59,10 @@
[xml-str]
(find-xml xml-str [:token_info :token]))

(defn parse-bearer-token
[body]
(:access_token (json/parse-string body true)))

(defn parse-username
"Parse the XML that is returned when querying the CMR Access Control API for
the username associated with the token."
Expand All @@ -58,27 +71,73 @@
(find-xml [:token_info :user_name])
first))

(defn parse-lp-username
[body]
(:uid (json/parse-string body true)))

(defn add-basic-auth
([user pass]
(add-basic-auth {} user pass))
([req user pass]
(assoc req :basic-auth [user pass])))

(defn get-client-token
"Get the CMR client bearer token from from the cache."
[system]
(let [base-url (config/get-edl-rest-url system)
url (str base-url client-token-resource)]
(request/async-post
url
(-> {}
(request/add-accept "application/json")
(request/add-basic-auth (config/get-edl-username system) (config/get-edl-password system)))
{}
#(response/general-response-handler % response/error-handler parse-bearer-token))))

(defn verify-edl-token-locally
"Uses the EDL public key to verify jwt tokens locally."
[system token]
(try
(log/trace "edl public key= " (config/get-jwt-public-key system))
(let [public-key (buddy-keys/jwk->public-key (json/parse-string (config/get-jwt-public-key system) true))
bearer-stripped-token (string/replace token #"Bearer\W+" "")
decrypted-token (jwt/unsign bearer-stripped-token public-key {:alg :rs256})]
{:body (:uid decrypted-token)})
(catch Exception ex
(log/error (.getMessage ex))
(log/debug "JWT local token verification failed, trying launchpad instead."))))

(defn get-token-info
"Query the CMR Access Control API for information assocated with the given
token."
[base-url token]
(let [url (str base-url token-info-resource)
data (str "id=" token)]
(log/trace "Making token-info query to ECHO REST:" url)
(request/async-post
url
(-> {:body data}
(http/add-options)
(request/add-token-header token)
(request/add-form-ct))
{}
#(response/body-only-response-handler % response/error-handler parse-username))))
[system token]
(let [base-url (config/get-edl-rest-url system)
url (str base-url token-info-resource)]
(log/trace "Decoding jwt token to get token info")
(if-let [token-info (verify-edl-token-locally system token)]
(do
(log/trace "user info found for jwt token, token-info=" token-info)
(deliver (promise) token-info))
(let [bearer-token @(get-client-token system)]
(if (:errors bearer-token)
(deliver (promise) bearer-token)
(do
(log/trace "retrieved cmr client token= " bearer-token)
(log/trace "user info not found for jwt token, attempting to authenticate user via launchpad")
(request/async-post
url
(-> {:body (str "token=" token)}
(http/add-options)
(request/add-token-header (str "Bearer " (:body bearer-token))))
{}
#(response/general-response-handler % response/error-handler parse-lp-username))))))))

(defn ->user
"Given a token, return the associated user name."
[base-url token]
(let [result @(get-token-info base-url token)
[system token]
(let [result @(get-token-info system token)
errors (:errors result)]
(log/trace "result=" result)
(if errors
(throw (ex-info (first errors) result))
result)))
(:body result))))
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
([req field value]
(assoc-in req [:headers field] value)))

(defn add-basic-auth
([user pass]
(add-basic-auth {} user pass))
([req user pass]
(assoc req :basic-auth [user pass])))

(defn add-accept
([value]
(add-accept {} value))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@

(defn json-errors
[body]
(:errors (:body (parse-json-result body))))
(or (:errors (parse-json-result body))
(seq (remove nil? [(:error (parse-json-result body))]))))

(defn parse-xml-body
[body]
Expand Down Expand Up @@ -174,6 +175,7 @@
(log/debug "Handling client response ...")
(log/trace "headers:" headers)
(log/trace "body:" body)
(log/trace "status:" status)
(cond error
(do
(log/error error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
(caching/lookup
system
(token/user-id-key token)
#(token/->user (config/get-echo-rest-url system) token))
#(token/->user system token))
(catch Exception e
(log/error e)
{:errors (base-errors/exception-data e)})))
Expand Down Expand Up @@ -131,7 +131,7 @@
(if-let [user-token (token/extract request)]
(let [user-lookup (cached-user system user-token)
errors (:errors user-lookup)]
(log/debug "ECHO token provided; proceeding ...")
(log/debug "Token provided; proceeding ...")
(log/trace "user-lookup:" user-lookup)
(if errors
(do
Expand All @@ -156,7 +156,7 @@
user-token
user-lookup)))))
(do
(log/warn "ECHO token not provided for protected resource")
(log/warn "Valid token not provided for protected resource")
(response/not-allowed errors/token-required))))

(defn check-route-access
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{:api-version "v3"
:default-content-type "json"
:auth-caching {:init {}
:auth-caching {:jwt-public-key "{
\"kty\": \"RSA\",
\"n\": \"3LopSyeoSZZGorSPjk4mMbR0ybVSLvfrONGSGXCNXE6ScX9Y1QC_zV8fVeh4XO8tYDi9CgzqK3Nhjsd5KI0ZzTI8Lf52tyr7OzebZXGZpMvyvdp59wlbPL4WFkIHvWFpgypSrTQIRENKaYW_yQB9srq6JpUx14aRG5TpiBuPqgnGM-qBqPvLq5LX9kVhqbV46TuZd9uPn_gISut7A7K3Y5S24DZd3ebxXPap1cn6-mIY30QG5oYmVlMZxdVPhnTzjj4ZNsfyKSRKq3F_UapEr4ynhr-ONgj8HyozyFqTpUn3o8pKAMVaOEfZmRlqb3jnQknbcsJ9fNxmWUUZr_PysQ\",
\"e\": \"AQAB\",
\"kid\": \"edljwtpubkey_sit\"
}"
:init {}
:ttl {:minutes 60}
:lru {:threshold 1000}
:dumpfile "data/cache/authz-dump.edn"}
Expand All @@ -13,6 +19,9 @@
:relative {:root {:url "/access-control"}}}}
:concept {
:variable {:version "1.1"}}
:edl {:rest
{:protocol "https"
:host "sit.urs.earthdata.nasa.gov"}}
:echo {:rest {:protocol "https"
:host "cmr.sit.earthdata.nasa.gov"
:context "/legacy-services/rest"}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
;; Authorization

(def no-permissions "You do not have permissions to access that resource.")
(def token-required "An ECHO token is required to access this resource.")
(def token-required "A valid token is required to access this resource.")

;; OUS - General

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
{:api-version "v3"
:default-content-type "json"
:auth-caching {:init {}
:auth-caching {:jwt-public-key "{
\"kty\": \"RSA\",
\"n\": \"3LopSyeoSZZGorSPjk4mMbR0ybVSLvfrONGSGXCNXE6ScX9Y1QC_zV8fVeh4XO8tYDi9CgzqK3Nhjsd5KI0ZzTI8Lf52tyr7OzebZXGZpMvyvdp59wlbPL4WFkIHvWFpgypSrTQIRENKaYW_yQB9srq6JpUx14aRG5TpiBuPqgnGM-qBqPvLq5LX9kVhqbV46TuZd9uPn_gISut7A7K3Y5S24DZd3ebxXPap1cn6-mIY30QG5oYmVlMZxdVPhnTzjj4ZNsfyKSRKq3F_UapEr4ynhr-ONgj8HyozyFqTpUn3o8pKAMVaOEfZmRlqb3jnQknbcsJ9fNxmWUUZr_PysQ\",
\"e\": \"AQAB\",
\"kid\": \"edljwtpubkey_sit\"
}"
:init {}
:ttl {:minutes 60}
:lru {:threshold 1000}
:dumpfile "data/cache/authz-dump.edn"}
Expand All @@ -13,6 +19,9 @@
:relative {:root {:url "/access-control"}}}}
:concept {
:variable {:version "1.1"}}
:edl {:rest
{:protocol "https"
:host "sit.urs.earthdata.nasa.gov"}}
:echo {:rest {:protocol "https"
:host "cmr.sit.earthdata.nasa.gov"
:context "/legacy-services/rest"}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@
(let [response @(httpc/get (format "http://localhost:%s/service-bridge/ping"
(test-system/http-port)))]
(is (= 403 (:status response)))
(is (= {:errors ["An ECHO token is required to access this resource."]}
(is (= {:errors ["A valid token is required to access this resource."]}
(response/parse-json-result (:body response))))))
(testing "v2 routes that don't exist in v1 ..."
(let [response @(httpc/get (format "http://localhost:%s/service-bridge/cache"
(test-system/http-port))
(-> {}
(request/add-token-header
(util/get-sit-token))
(request/add-accept
"application/vnd.cmr-service-bridge.v1+json")))]
(-> {}
(request/add-token-header
(util/get-sit-token))
(request/add-accept
"application/vnd.cmr-service-bridge.v1+json")))]
(is (= 404 (:status response)))))
(testing "v2 routes ..."
#_(let [response @(httpc/get (format "http://localhost:%s/service-bridge/cache/auth"
Expand All @@ -55,7 +55,7 @@
(util/get-sit-token))
(request/add-accept
"application/vnd.cmr-service-bridge.v2+json")))]
(is (= 200 (:status response))))
(is (= 200 (:status response))))
;; XXX This test is currently failing; this has happened before with soft references
;; see the code comment above cmr.http.kit.response/soft-reference->json! ...
#_(let [response @(httpc/get (format "http://localhost:%s/service-bridge/cache/concept"
Expand All @@ -65,7 +65,7 @@
(util/get-sit-token))
(request/add-accept
"application/vnd.cmr-service-bridge.v2+json")))]
(is (= 200 (:status response))))))
(is (= 200 (:status response))))))

(deftest testing-routes
(is 401
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,5 @@
(test-system/http-port)
collection-id))]
(is (= 403 (:status response)))
(is (= {:errors ["An ECHO token is required to access this resource."]}
(is (= {:errors ["A valid token is required to access this resource."]}
(response/parse-json-result (:body response)))))))

0 comments on commit 90dfd26

Please sign in to comment.