/api/api-key endpoints for CRUD management of API Keys | (ns metabase.api.api-key (:require [compojure.core :refer [POST GET PUT DELETE]] [metabase.api.common :as api] [metabase.events :as events] [metabase.models.api-key :as api-key] [metabase.models.permissions-group :as perms-group] [metabase.models.user :as user] [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.malli.schema :as ms] [metabase.util.secret :as u.secret] [toucan2.core :as t2])) |
(defn- maybe-expose-key [api-key] (if (contains? api-key :unmasked_key) (update api-key :unmasked_key u.secret/expose) api-key)) | |
Takes an ApiKey and hydrates/selects keys as necessary to put it into a standard form for responses | (defn- present-api-key [api-key] (-> api-key (t2/hydrate :group :updated_by) (select-keys [:created_at :updated_at :updated_by :id :group :unmasked_key :name :masked_key]) (maybe-expose-key) (update :updated_by #(select-keys % [:common_name :id])))) |
(defn- key-with-unique-prefix [] (u/auto-retry 5 (let [api-key (api-key/generate-key) prefix (api-key/prefix (u.secret/expose api-key))] ;; we could make this more efficient by generating 5 API keys up front and doing one select to remove any ;; duplicates. But a duplicate should be rare enough to just do multiple queries for now. (if-not (t2/exists? :model/ApiKey :key_prefix prefix) api-key (throw (ex-info (tru "could not generate key with unique prefix") {})))))) | |
(defn- with-updated-by [api-key] (assoc api-key :updated_by_id api/*current-user-id*)) | |
(defn- with-creator [api-key] (assoc api-key :creator_id api/*current-user-id*)) | |
/ | (api/defendpoint POST "Create a new API key (and an associated `User`) with the provided name and group ID." [:as {{:keys [group_id name] :as _body} :body}] {group_id ms/PositiveInt name ms/NonBlankString} (api/check-superuser) (api/checkp (not (t2/exists? :model/ApiKey :name name)) "name" "An API key with this name already exists.") (let [unhashed-key (key-with-unique-prefix) email (format "api-key-user-%s@api-key.invalid" (random-uuid))] (t2/with-transaction [_conn] (let [user (first (t2/insert-returning-instances! :model/User {:email email :first_name name :last_name "" :type :api-key :password (str (random-uuid))}))] (user/set-permissions-groups! user [(perms-group/all-users) group_id]) (let [api-key (-> (t2/insert-returning-instance! :model/ApiKey (-> {:user_id (u/the-id user) :name name :unhashed_key unhashed-key} with-creator with-updated-by)) (t2/hydrate :group :updated_by))] (events/publish-event! :event/api-key-create {:object api-key :user-id api/*current-user-id*}) (present-api-key (assoc api-key :unmasked_key unhashed-key))))))) |
/count | (api/defendpoint GET "Get the count of API keys in the DB with the default scope." [:as _body] (api/check-superuser) (t2/count :model/ApiKey :scope nil)) |
/:id | (api/defendpoint PUT "Update an API key by changing its group and/or its name" [id :as {{:keys [group_id name] :as _body} :body}] {id ms/PositiveInt group_id [:maybe ms/PositiveInt] name [:maybe ms/NonBlankString]} (api/check-superuser) (let [api-key-before (-> (t2/select-one :model/ApiKey :id id) ;; hydrate the group_name for audit logging (t2/hydrate :group) (api/check-404))] (t2/with-transaction [_conn] (when group_id (let [user (-> api-key-before (t2/hydrate :user) :user)] (user/set-permissions-groups! user [(perms-group/all-users) {:id group_id}]))) (when name ;; A bit of a pain to keep these in sync, but oh well. (t2/update! :model/User (:user_id api-key-before) {:first_name name :last_name ""}) (t2/update! :model/ApiKey id (with-updated-by {:name name})))) (let [updated-api-key (-> (t2/select-one :model/ApiKey :id id) (t2/hydrate :group :updated_by))] (events/publish-event! :event/api-key-update {:object updated-api-key :previous-object api-key-before :user-id api/*current-user-id*}) (present-api-key updated-api-key)))) |
/:id/regenerate | (api/defendpoint PUT "Regenerate an API Key" [id] {id ms/PositiveInt} (api/check-superuser) (let [api-key-before (-> (t2/select-one :model/ApiKey id) (t2/hydrate :group) (api/check-404)) unhashed-key (key-with-unique-prefix) api-key-after (assoc api-key-before :unhashed_key unhashed-key :key_prefix (api-key/prefix (u.secret/expose unhashed-key)))] (t2/update! :model/ApiKey :id id (with-updated-by (select-keys api-key-after [:unhashed_key]))) (events/publish-event! :event/api-key-regenerate {:object api-key-after :previous-object api-key-before :user-id api/*current-user-id*}) (present-api-key (assoc api-key-after :unmasked_key unhashed-key :masked_key (api-key/mask unhashed-key))))) |
/ | (api/defendpoint GET "Get a list of API keys with the default scope. Non-paginated." [] (api/check-superuser) (let [api-keys (t2/hydrate (t2/select :model/ApiKey :scope nil) :group :updated_by)] (map present-api-key api-keys))) |
/:id | (api/defendpoint DELETE "Delete an ApiKey" [id] {id ms/PositiveInt} (api/check-superuser) (let [api-key (-> (t2/select-one :model/ApiKey id) (t2/hydrate :group) (api/check-404))] (t2/with-transaction [_tx] (t2/delete! :model/ApiKey id) (t2/update! :model/User (:user_id api-key) {:is_active false})) (events/publish-event! :event/api-key-delete {:object api-key :user-id api/*current-user-id*}) api/generic-204-no-content)) |
(api/define-routes) | |