diff --git a/src/terrain/clients/apps/raw.clj b/src/terrain/clients/apps/raw.clj index f5aed9cc..4b067926 100644 --- a/src/terrain/clients/apps/raw.clj +++ b/src/terrain/clients/apps/raw.clj @@ -1277,8 +1277,8 @@ :content-type :json})))) (defn record-login - [ip-address user-agent] - (let [params (remove-nil-values {:ip-address ip-address :user-agent user-agent})] + [ip-address session-id login-time] + (let [params (remove-nil-values {:ip-address ip-address :session-id session-id :login-time login-time})] (:body (client/post (apps-url "users" "login") (disable-redirects diff --git a/src/terrain/clients/keycloak/admin.clj b/src/terrain/clients/keycloak/admin.clj new file mode 100644 index 00000000..bd143f3b --- /dev/null +++ b/src/terrain/clients/keycloak/admin.clj @@ -0,0 +1,61 @@ +(ns terrain.clients.keycloak.admin + (:require [cemerick.url :as curl] + [clj-http.client :as http] + [terrain.util.config :as config])) + +(defn- keycloak-admin-url + "Builds a Keycloak admin API URL with the given path components." + [& components] + (str (apply curl/url (config/keycloak-admin-base-uri) "realms" (config/keycloak-realm) components))) + +(defn- keycloak-admin-token-url + "Like keycloak-admin-url but for the 'master' realm to get a token to use with the API" + [& components] + (str (apply curl/url (config/keycloak-admin-base-uri) "realms" "master" components))) + +(defn get-token + "Obtains authorization token data for the admin service account. You'll probably want the access_token field in the return value." + [] + (:body (http/post (keycloak-admin-token-url "protocol" "openid-connect" "token") + {:form-params {:grant_type "client_credentials" + :client_id (config/keycloak-client-id) + :client_secret (config/keycloak-client-secret)} + :as :json}))) + +; https://www.keycloak.org/docs-api/26.0.5/rest-api/#_get_adminrealmsrealmusers +(defn get-user + "Obtains user information from keycloak + + This will be a map including keys at least :username and :id, which should be + what we need to make further requests" + ([username] + (get-user username (:access_token (get-token)))) + ([username token] + (let [user-data (http/get (keycloak-admin-url "users") + {:query-params {:username username + :exact true} + :headers {:authorization (str "Bearer " token)} + :as :json})] + ; the 'exact' query parameter doesn't seem to work on all keycloak versions, so we filter it + (->> user-data + (filter (fn [user] (= (:username user) username))) + first)))) + +; https://www.keycloak.org/docs-api/26.0.5/rest-api/#_get_adminrealmsrealmusersuser_idsessions +(defn get-user-session + "Obtains information about the user's current session from keycloak. + + This will be a list of maps, which will include user ID, ip address, session ID, and clients at least." + ([user-id] + (get-user-session user-id (:access_token (get-token)))) + ([user-id token] + (:body (http/get (keycloak-admin-url "users" user-id "sessions") + {:headers {:authorization (str "Bearer " token)} + :as :json})))) + +(defn get-user-session-by-username + "Same as `get-user-session`, but by username by way of a request to `get-user` first." + ([username] + (get-user-session-by-username username (:access_token (get-token)))) + ([username token] + (get-user-session (:id (get-user username token)) token))) diff --git a/src/terrain/services/bootstrap.clj b/src/terrain/services/bootstrap.clj index 73171ccf..93ce543c 100644 --- a/src/terrain/services/bootstrap.clj +++ b/src/terrain/services/bootstrap.clj @@ -5,6 +5,7 @@ [slingshot.slingshot :refer [try+]] [terrain.auth.user-attributes :refer [current-user]] [terrain.clients.apps.raw :as apps-client] + [terrain.clients.keycloak.admin :as kc-client] [terrain.clients.data-info :as data-info-client] [terrain.services.user-prefs :as prefs] [terrain.util.service :as service])) @@ -33,9 +34,14 @@ {:error (str (:throwable &throw-context))}))) (defn- get-login-session - [ip-address user-agent] + [username] (trap-bootstrap-request - #(select-keys (apps-client/record-login ip-address user-agent) [:login_time :auth_redirect]))) + #(let [kc-resp (kc-client/get-user-session-by-username username) + current-session (first kc-resp) ;; TODO: choose most recent start/access using our known client ID + ] + (select-keys (apps-client/record-login (:ipAddress current-session nil) + (:id current-session nil) + (:start current-session nil)) [:login_time :auth_redirect])))) (defn- get-apps-info [] @@ -60,7 +66,7 @@ [ip-address user-agent] (assertions/assert-valid user-agent "Missing or empty request parameter: user-agent") (let [{user :shortUsername :keys [email firstName lastName username]} current-user - login-session (future (get-login-session ip-address user-agent)) + login-session (future (get-login-session username)) apps-info (future (get-apps-info)) data-info (future (get-user-data-info user)) preferences (future (get-user-prefs username))] diff --git a/src/terrain/util/config.clj b/src/terrain/util/config.clj index 483842fc..c54ceb7f 100644 --- a/src/terrain/util/config.clj +++ b/src/terrain/util/config.clj @@ -696,6 +696,24 @@ [props config-valid configs] "terrain.keycloak.client-secret") +(declare keycloak-admin-base-uri) +(cc/defprop-optstr keycloak-admin-base-uri + "The base URI to use for administrative requests to Keycloak." + [props config-valid configs] + "terrain.keycloak.admin-base-uri" "https://keycloaktest2.cyverse.org/auth/admin") + +(declare keycloak-admin-client-id) +(cc/defprop-str keycloak-admin-client-id + "The Keycloak admin client ID to use." + [props config-valid configs] + "terrain.keycloak.admin-client-id") + +(declare keycloak-admin-client-secret) +(cc/defprop-str keycloak-admin-client-secret + "The keycloak admin client secret to use." + [props config-valid configs] + "terrain.keycloak.admin-client-secret") + (declare dashboard-aggregator-url) (cc/defprop-optstr dashboard-aggregator-url "The URL to the dashboard-aggregator service."