Metabase API endpoints for viewing publicly-accessible Cards and Dashboards. | (ns metabase.api.public (:require [compojure.core :refer [GET]] [medley.core :as m] [metabase.actions.core :as actions] [metabase.analytics.snowplow :as snowplow] [metabase.api.card :as api.card] [metabase.api.common :as api] [metabase.api.common.validation :as validation] [metabase.api.dashboard :as api.dashboard] [metabase.api.dataset :as api.dataset] [metabase.api.field :as api.field] [metabase.db.query :as mdb.query] [metabase.events :as events] [metabase.lib.schema.id :as lib.schema.id] [metabase.lib.schema.info :as lib.schema.info] [metabase.lib.util.match :as lib.util.match] [metabase.models.action :as action] [metabase.models.card :as card :refer [Card]] [metabase.models.dashboard :refer [Dashboard]] [metabase.models.dimension :refer [Dimension]] [metabase.models.field :refer [Field]] [metabase.models.interface :as mi] [metabase.models.params :as params] [metabase.query-processor.card :as qp.card] [metabase.query-processor.dashboard :as qp.dashboard] [metabase.query-processor.error-type :as qp.error-type] [metabase.query-processor.middleware.constraints :as qp.constraints] [metabase.query-processor.middleware.permissions :as qp.perms] [metabase.query-processor.pipeline :as qp.pipeline] [metabase.query-processor.pivot :as qp.pivot] [metabase.query-processor.streaming :as qp.streaming] [metabase.request.core :as request] [metabase.util :as u] [metabase.util.embed :as embed] [metabase.util.i18n :refer [tru]] [metabase.util.json :as json] [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms] [throttle.core :as throttle] [toucan2.core :as t2]) (:import (clojure.lang ExceptionInfo))) |
(set! *warn-on-reflection* true) | |
(def ^:private ^:const ^Integer default-embed-max-height 800) (def ^:private ^:const ^Integer default-embed-max-width 1024) | |
-------------------------------------------------- Public Cards -------------------------------------------------- | |
Update On native queries parameters exists in 2 forms: - parameters - dataset_query.native.template-tags In most cases, these 2 are sync, meaning, if you have a template-tag, there will be a parameter. However, since card.parameters is a recently added feature, there may be instances where a template-tag is not present in the parameters. This function ensures that all template-tags are converted to parameters and added to card.parameters. | (defn combine-parameters-and-template-tags [{:keys [parameters] :as card}] (let [template-tag-parameters (card/template-tag-parameters card) id->template-tags-parameter (m/index-by :id template-tag-parameters) id->parameter (m/index-by :id parameters)] (assoc card :parameters (vals (reduce-kv (fn [acc id parameter] ;; order importance: we want the info from `template-tag` to be merged last (update acc id #(merge % parameter))) id->parameter id->template-tags-parameter))))) |
Remove everyting from public | (defn- remove-card-non-public-columns [card] ;; We need to check this to resolve params - we set `request/as-admin` there (if qp.perms/*param-values-query* card (mi/instance Card (u/select-nested-keys card [:id :name :description :display :visualization_settings :parameters [:dataset_query :type [:native :template-tags]]])))) |
Return a public Card matching key-value | (defn public-card [& conditions] (binding [params/*ignore-current-user-perms-and-return-all-field-values* true] (-> (api/check-404 (apply t2/select-one [Card :id :dataset_query :description :display :name :parameters :visualization_settings] :archived false, conditions)) remove-card-non-public-columns combine-parameters-and-template-tags (t2/hydrate :param_values :param_fields)))) |
(defn- card-with-uuid [uuid] (public-card :public_uuid uuid)) | |
/card/:uuid | (api/defendpoint GET "Fetch a publicly-accessible Card an return query results as well as `:card` information. Does not require auth credentials. Public sharing must be enabled." [uuid] {uuid ms/UUIDString} (validation/check-public-sharing-enabled) (u/prog1 (card-with-uuid uuid) (events/publish-event! :event/card-read {:object-id (:id <>), :user-id api/*current-user-id*, :context :question}))) |
Transform results to be suitable for a public endpoint | (defmulti ^:private transform-qp-result {:arglists '([results])} :status) |
(defmethod transform-qp-result :default [x] x) | |
(defmethod transform-qp-result :completed [results] (u/select-nested-keys results [[:data :cols :rows :rows_truncated :insights :requested_timezone :results_timezone] [:json_query :parameters] :status])) | |
(defmethod transform-qp-result :failed [{error-type :error_type, :as results}] ;; if the query failed instead, unless the error type is specified and is EXPLICITLY allowed to be shown for embeds, ;; instead of returning anything about the query just return a generic error message (merge (select-keys results [:status :error :error_type]) (when-not (qp.error-type/show-in-embeds? error-type) {:error (tru "An error occurred while running the query.")}))) | |
Create the | (defn- process-query-for-card-with-id-run-fn [qp export-format] (fn run [query info] (qp.streaming/streaming-response [rff export-format (u/slugify (:card-name info))] (binding [qp.pipeline/*result* (comp qp.pipeline/*result* transform-qp-result)] (request/as-admin (qp (update query :info merge info) rff)))))) |
(mu/defn- export-format->context :- ::lib.schema.info/context [export-format] (case export-format "csv" :public-csv-download "xlsx" :public-xlsx-download "json" :public-json-download :public-question)) | |
Run the query belonging to Card with | (mu/defn process-query-for-card-with-id [card-id :- ::lib.schema.id/card export-format parameters & {:keys [qp] :or {qp qp.card/process-query-for-card-default-qp} :as options}] ;; run this query with full superuser perms ;; ;; we actually need to bind the current user perms here twice, once so `card-api` will have the full perms when it ;; tries to do the `read-check`, and a second time for when the query is ran (async) so the QP middleware will have ;; the correct perms (request/as-admin (m/mapply qp.card/process-query-for-card card-id export-format :parameters parameters :context (export-format->context export-format) :qp qp :make-run process-query-for-card-with-id-run-fn options))) |
Run query for a public Card with UUID. If public sharing is not enabled, this throws an exception. Returns a
| (defn ^:private process-query-for-card-with-public-uuid [uuid export-format parameters & options] (validation/check-public-sharing-enabled) (let [card-id (api/check-404 (t2/select-one-pk Card :public_uuid uuid, :archived false))] (apply process-query-for-card-with-id card-id export-format parameters options))) |
/card/:uuid/query | (api/defendpoint GET "Fetch a publicly-accessible Card an return query results as well as `:card` information. Does not require auth credentials. Public sharing must be enabled." [uuid parameters] {uuid ms/UUIDString parameters [:maybe ms/JSONString]} (process-query-for-card-with-public-uuid uuid :api (json/decode+kw parameters))) |
/card/:uuid/query/:export-format | (api/defendpoint GET "Fetch a publicly-accessible Card and return query results in the specified format. Does not require auth credentials. Public sharing must be enabled." [uuid export-format :as {{:keys [parameters format_rows pivot_results]} :params}] {uuid ms/UUIDString export-format api.dataset/ExportFormat format_rows [:maybe :boolean] pivot_results [:maybe :boolean] parameters [:maybe ms/JSONString]} (process-query-for-card-with-public-uuid uuid export-format (json/decode+kw parameters) :constraints nil :middleware {:process-viz-settings? true :js-int-to-string? false :format-rows? (or format_rows false) :pivot? (or pivot_results false)})) |
----------------------------------------------- Public Dashboards ------------------------------------------------ | |
The only keys for an action that should be visible to the general public. | (def ^:private action-public-keys #{:name :id :database_id ;; needed to check if the database has actions enabled on the frontend :visualization_settings :parameters}) |
Returns a public version of | (defn- public-action [action] (let [hidden-parameter-ids (->> (get-in action [:visualization_settings :fields]) vals (keep (fn [x] (when (true? (:hidden x)) (:id x)))) set)] (-> action (update :parameters (fn [parameters] (remove #(contains? hidden-parameter-ids (:id %)) parameters))) (update-in [:visualization_settings :fields] (fn [fields] (m/remove-keys hidden-parameter-ids fields))) (select-keys action-public-keys)))) |
Return a public Dashboard matching key-value | (defn public-dashboard [& conditions] {:pre [(even? (count conditions))]} (binding [params/*ignore-current-user-perms-and-return-all-field-values* true params/*field-id-context* (atom params/empty-field-id-context)] (-> (api/check-404 (apply t2/select-one [Dashboard :name :description :id :parameters :auto_apply_filters :width], :archived false, conditions)) (t2/hydrate [:dashcards :card :series :dashcard/action] :tabs :param_values :param_fields) api.dashboard/add-query-average-durations (update :dashcards (fn [dashcards] (for [dashcard dashcards] (-> (select-keys dashcard [:id :card :card_id :dashboard_id :series :col :row :size_x :dashboard_tab_id :size_y :parameter_mappings :visualization_settings :action]) (update :card remove-card-non-public-columns) (update :series (fn [series] (for [series series] (remove-card-non-public-columns series)))) (m/update-existing :action public-action)))))))) |
(defn- dashboard-with-uuid [uuid] (public-dashboard :public_uuid uuid)) | |
/dashboard/:uuid | (api/defendpoint GET "Fetch a publicly-accessible Dashboard. Does not require auth credentials. Public sharing must be enabled." [uuid] {uuid ms/UUIDString} (validation/check-public-sharing-enabled) (u/prog1 (dashboard-with-uuid uuid) (events/publish-event! :event/dashboard-read {:object-id (:id <>), :user-id api/*current-user-id*}))) |
Return the results of running a query for Card with
Throws a 404 immediately if the Card isn't part of the Dashboard. Returns a | (defn process-query-for-dashcard {:arglists '([& {:keys [dashboard-id card-id dashcard-id export-format parameters] :as options}])} [& {:keys [export-format parameters qp] :or {qp qp.card/process-query-for-card-default-qp export-format :api} :as options}] (let [options (merge {:context :public-dashboard :constraints (qp.constraints/default-query-constraints)} options {:parameters (cond-> parameters (string? parameters) json/decode+kw) :export-format export-format :qp qp :make-run process-query-for-card-with-id-run-fn})] ;; Run this query with full superuser perms. We don't want the various perms checks failing because there are no ;; current user perms; if this Dashcard is public you're by definition allowed to run it without a perms check ;; anyway (request/as-admin (m/mapply qp.dashboard/process-query-for-dashcard options)))) |
/dashboard/:uuid/dashcard/:dashcard-id/card/:card-id | (api/defendpoint GET "Fetch the results for a Card in a publicly-accessible Dashboard. Does not require auth credentials. Public sharing must be enabled." [uuid card-id dashcard-id parameters] {uuid ms/UUIDString dashcard-id ms/PositiveInt card-id ms/PositiveInt parameters [:maybe ms/JSONString]} (validation/check-public-sharing-enabled) (api/check-404 (t2/select-one-pk :model/Card :id card-id :archived false)) (let [dashboard-id (api/check-404 (t2/select-one-pk Dashboard :public_uuid uuid, :archived false))] (u/prog1 (process-query-for-dashcard :dashboard-id dashboard-id :card-id card-id :dashcard-id dashcard-id :export-format :api :parameters parameters) (events/publish-event! :event/card-read {:object-id card-id, :user-id api/*current-user-id*, :context :dashboard})))) |
(api/defendpoint POST ["/dashboard/:uuid/dashcard/:dashcard-id/card/:card-id/:export-format" :export-format api.dataset/export-format-regex] "Fetch the results of running a publicly-accessible Card belonging to a Dashboard and return the data in one of the export formats. Does not require auth credentials. Public sharing must be enabled." [uuid card-id dashcard-id parameters export-format :as {{:keys [format_rows pivot_results]} :params}] {uuid ms/UUIDString dashcard-id ms/PositiveInt card-id ms/PositiveInt parameters [:maybe ms/JSONString] format_rows [:maybe ms/BooleanValue] pivot_results [:maybe ms/BooleanValue] export-format (into [:enum] api.dataset/export-formats)} (validation/check-public-sharing-enabled) (api/check-404 (t2/select-one-pk :model/Card :id card-id :archived false)) (let [dashboard-id (api/check-404 (t2/select-one-pk Dashboard :public_uuid uuid, :archived false))] (u/prog1 (process-query-for-dashcard :dashboard-id dashboard-id :card-id card-id :dashcard-id dashcard-id :export-format export-format :parameters parameters :constraints nil :middleware {:process-viz-settings? true :format-rows? (or format_rows false) :pivot? (or pivot_results false)})))) | |
/dashboard/:uuid/dashcard/:dashcard-id/execute | (api/defendpoint GET "Fetches the values for filling in execution parameters. Pass PK parameters and values to select." [uuid dashcard-id parameters] {uuid ms/UUIDString dashcard-id ms/PositiveInt parameters ms/JSONString} (validation/check-public-sharing-enabled) (api/check-404 (t2/select-one-pk Dashboard :public_uuid uuid :archived false)) (actions/fetch-values (api/check-404 (action/dashcard->action dashcard-id)) (json/decode parameters))) |
(def ^:private dashcard-execution-throttle (throttle/make-throttler :dashcard-id :attempts-threshold 5000)) | |
/dashboard/:uuid/dashcard/:dashcard-id/execute | (api/defendpoint POST "Execute the associated Action in the context of a `Dashboard` and `DashboardCard` that includes it. `parameters` should be the mapped dashboard parameters with values." [uuid dashcard-id :as {{:keys [parameters], :as _body} :body}] {uuid ms/UUIDString dashcard-id ms/PositiveInt parameters [:maybe [:map-of :keyword :any]]} (let [throttle-message (try (throttle/check dashcard-execution-throttle dashcard-id) nil (catch ExceptionInfo e (get-in (ex-data e) [:errors :dashcard-id]))) throttle-time (when throttle-message (second (re-find #"You must wait ([0-9]+) seconds" throttle-message)))] (if throttle-message (cond-> {:status 429 :body throttle-message} throttle-time (assoc :headers {"Retry-After" throttle-time})) (do (validation/check-public-sharing-enabled) (let [dashboard-id (api/check-404 (t2/select-one-pk Dashboard :public_uuid uuid, :archived false))] ;; Run this query with full superuser perms. We don't want the various perms checks ;; failing because there are no current user perms; if this Dashcard is public ;; you're by definition allowed to run it without a perms check anyway (request/as-admin ;; Undo middleware string->keyword coercion (actions/execute-dashcard! dashboard-id dashcard-id (update-keys parameters name)))))))) |
/oembed | (api/defendpoint GET "oEmbed endpoint used to retreive embed code and metadata for a (public) Metabase URL." [url format maxheight maxwidth] ;; the format param is not used by the API, but is required as part of the oEmbed spec: http://oembed.com/#section2 ;; just return an error if `format` is specified and it's anything other than `json`. {url ms/NonBlankString format [:maybe [:enum "json"]] maxheight [:maybe ms/IntString] maxwidth [:maybe ms/IntString]} (let [height (if maxheight (Integer/parseInt maxheight) default-embed-max-height) width (if maxwidth (Integer/parseInt maxwidth) default-embed-max-width)] {:version "1.0" :type "rich" :width width :height height :html (embed/iframe url width height)})) |
----------------------------------------------- Public Action ------------------------------------------------ | |
/action/:uuid | (api/defendpoint GET "Fetch a publicly-accessible Action. Does not require auth credentials. Public sharing must be enabled." [uuid] {uuid ms/UUIDString} (validation/check-public-sharing-enabled) (let [action (api/check-404 (action/select-action :public_uuid uuid :archived false))] (actions/check-actions-enabled! action) (public-action action))) |
+----------------------------------------------------------------------------------------------------------------+ | FieldValues, Search, Remappings | +----------------------------------------------------------------------------------------------------------------+ | |
-------------------------------------------------- Field Values -------------------------------------------------- | |
Get the IDs of all Fields referenced by an MBQL | (defn- query->referenced-field-ids [query] (lib.util.match/match (:query query) [:field id _] id)) |
Return a set of all Field IDs referenced by | (defn- card->referenced-field-ids [card] (set (concat (query->referenced-field-ids (:dataset_query card)) (params/card->template-tag-field-ids card)))) |
Check to make sure the query for Card with | (defn- check-field-is-referenced-by-card [field-id card-id] (let [card (api/check-404 (t2/select-one [Card :dataset_query] :id card-id)) referenced-field-ids (card->referenced-field-ids card)] (api/check-404 (contains? referenced-field-ids field-id)))) |
Check whether a search Field is allowed to be used in conjunction with another Field. A search Field is allowed if any of the following conditions is true:
If none of these conditions are met, you are not allowed to use the search field in combination with the other field, and an 400 exception will be thrown. | (defn- check-search-field-is-allowed [field-id search-field-id] {:pre [(integer? field-id) (integer? search-field-id)]} (api/check-400 (or (= field-id search-field-id) (t2/exists? Dimension :field_id field-id, :human_readable_field_id search-field-id) ;; just do a couple small queries to figure this out, we could write a fancy query to join Field against itself ;; and do this in one but the extra code complexity isn't worth it IMO (when-let [table-id (t2/select-one-fn :table_id Field :id field-id, :semantic_type (mdb.query/isa :type/PK))] (t2/exists? Field :id search-field-id, :table_id table-id, :semantic_type (mdb.query/isa :type/Name)))))) |
Check that | (defn- check-field-is-referenced-by-dashboard [field-id dashboard-id] (let [dashboard (-> (t2/select-one Dashboard :id dashboard-id) api/check-404 (t2/hydrate [:dashcards :card])) param-field-ids (params/dashcards->param-field-ids (:dashcards dashboard))] (api/check-404 (contains? param-field-ids field-id)))) |
Return the FieldValues for a Field with | (defn card-and-field-id->values [card-id field-id] (check-field-is-referenced-by-card field-id card-id) (api.field/field->values (t2/select-one Field :id field-id))) |
/card/:uuid/field/:field-id/values | (api/defendpoint GET "Fetch FieldValues for a Field that is referenced by a public Card." [uuid field-id] {uuid ms/UUIDString field-id ms/PositiveInt} (validation/check-public-sharing-enabled) (let [card-id (t2/select-one-pk Card :public_uuid uuid, :archived false)] (card-and-field-id->values card-id field-id))) |
Return the FieldValues for a Field with | (defn dashboard-and-field-id->values [dashboard-id field-id] (check-field-is-referenced-by-dashboard field-id dashboard-id) (api.field/field->values (t2/select-one Field :id field-id))) |
/dashboard/:uuid/field/:field-id/values | (api/defendpoint GET "Fetch FieldValues for a Field that is referenced by a Card in a public Dashboard." [uuid field-id] {uuid ms/UUIDString field-id ms/PositiveInt} (validation/check-public-sharing-enabled) (let [dashboard-id (api/check-404 (t2/select-one-pk Dashboard :public_uuid uuid, :archived false))] (dashboard-and-field-id->values dashboard-id field-id))) |
--------------------------------------------------- Searching ---------------------------------------------------- | |
Wrapper for | (defn search-card-fields [card-id field-id search-id value limit] (check-field-is-referenced-by-card field-id card-id) (check-search-field-is-allowed field-id search-id) (api.field/search-values (t2/select-one Field :id field-id) (t2/select-one Field :id search-id) value limit)) |
Wrapper for | (defn search-dashboard-fields [dashboard-id field-id search-id value limit] (check-field-is-referenced-by-dashboard field-id dashboard-id) (check-search-field-is-allowed field-id search-id) (api.field/search-values (t2/select-one Field :id field-id) (t2/select-one Field :id search-id) value limit)) |
/card/:uuid/field/:field-id/search/:search-field-id | (api/defendpoint GET "Search for values of a Field that is referenced by a public Card." [uuid field-id search-field-id value limit] {uuid ms/UUIDString field-id ms/PositiveInt search-field-id ms/PositiveInt value ms/NonBlankString limit [:maybe ms/PositiveInt]} (validation/check-public-sharing-enabled) (let [card-id (t2/select-one-pk Card :public_uuid uuid, :archived false)] (search-card-fields card-id field-id search-field-id value limit))) |
/dashboard/:uuid/field/:field-id/search/:search-field-id | (api/defendpoint GET "Search for values of a Field that is referenced by a Card in a public Dashboard." [uuid field-id search-field-id value limit] {uuid ms/UUIDString field-id ms/PositiveInt search-field-id ms/PositiveInt value ms/NonBlankString limit [:maybe ms/PositiveInt]} (validation/check-public-sharing-enabled) (let [dashboard-id (api/check-404 (t2/select-one-pk Dashboard :public_uuid uuid, :archived false))] (search-dashboard-fields dashboard-id field-id search-field-id value limit))) |
--------------------------------------------------- Remappings --------------------------------------------------- | |
(defn- field-remapped-values [field-id remapped-field-id, ^String value-str] (let [field (api/check-404 (t2/select-one Field :id field-id)) remapped-field (api/check-404 (t2/select-one Field :id remapped-field-id))] (check-search-field-is-allowed field-id remapped-field-id) (api.field/remapped-value field remapped-field (api.field/parse-query-param-value-for-field field value-str)))) | |
Return the reampped Field values for a Field referenced by a Card. This explanation is almost useless, so see the
one in | (defn card-field-remapped-values [card-id field-id remapped-field-id, ^String value-str] (check-field-is-referenced-by-card field-id card-id) (field-remapped-values field-id remapped-field-id value-str)) |
Return the reampped Field values for a Field referenced by a Dashboard. This explanation is almost useless, so see
the one in | (defn dashboard-field-remapped-values [dashboard-id field-id remapped-field-id, ^String value-str] (check-field-is-referenced-by-dashboard field-id dashboard-id) (field-remapped-values field-id remapped-field-id value-str)) |
/card/:uuid/field/:field-id/remapping/:remapped-id | (api/defendpoint GET "Fetch remapped Field values. This is the same as `GET /api/field/:id/remapping/:remapped-id`, but for use with public Cards." [uuid field-id remapped-id value] {uuid ms/UUIDString field-id ms/PositiveInt remapped-id ms/PositiveInt value ms/NonBlankString} (validation/check-public-sharing-enabled) (let [card-id (api/check-404 (t2/select-one-pk Card :public_uuid uuid, :archived false))] (card-field-remapped-values card-id field-id remapped-id value))) |
/dashboard/:uuid/field/:field-id/remapping/:remapped-id | (api/defendpoint GET "Fetch remapped Field values. This is the same as `GET /api/field/:id/remapping/:remapped-id`, but for use with public Dashboards." [uuid field-id remapped-id value] {uuid ms/UUIDString field-id ms/PositiveInt remapped-id ms/PositiveInt value ms/NonBlankString} (validation/check-public-sharing-enabled) (let [dashboard-id (t2/select-one-pk Dashboard :public_uuid uuid, :archived false)] (dashboard-field-remapped-values dashboard-id field-id remapped-id value))) |
------------------------------------------------ Param Values ------------------------------------------------- | |
/card/:uuid/params/:param-key/values | (api/defendpoint GET "Fetch values for a parameter on a public card." [uuid param-key] {uuid ms/UUIDString param-key ms/NonBlankString} (validation/check-public-sharing-enabled) (let [card (t2/select-one Card :public_uuid uuid, :archived false)] (request/as-admin (api.card/param-values card param-key)))) |
/card/:uuid/params/:param-key/search/:query | (api/defendpoint GET "Fetch values for a parameter on a public card containing `query`." [uuid param-key query] {uuid ms/UUIDString param-key ms/NonBlankString query ms/NonBlankString} (validation/check-public-sharing-enabled) (let [card (t2/select-one Card :public_uuid uuid, :archived false)] (request/as-admin (api.card/param-values card param-key query)))) |
/dashboard/:uuid/params/:param-key/values | (api/defendpoint GET "Fetch filter values for dashboard parameter `param-key`." [uuid param-key :as {constraint-param-key->value :query-params}] {uuid ms/UUIDString param-key ms/NonBlankString} (let [dashboard (dashboard-with-uuid uuid)] (request/as-admin (binding [qp.perms/*param-values-query* true] (api.dashboard/param-values dashboard param-key constraint-param-key->value))))) |
/dashboard/:uuid/params/:param-key/search/:query | (api/defendpoint GET "Fetch filter values for dashboard parameter `param-key`, containing specified `query`." [uuid param-key query :as {constraint-param-key->value :query-params}] {uuid ms/UUIDString param-key ms/NonBlankString query ms/NonBlankString} (let [dashboard (dashboard-with-uuid uuid)] (request/as-admin (binding [qp.perms/*param-values-query* true] (api.dashboard/param-values dashboard param-key constraint-param-key->value query))))) |
----------------------------------------------------- Pivot Tables ----------------------------------------------- | |
/pivot/card/:uuid/query TODO -- why do these endpoints START with | (api/defendpoint GET "Fetch a publicly-accessible Card an return query results as well as `:card` information. Does not require auth credentials. Public sharing must be enabled." [uuid parameters] {uuid ms/UUIDString parameters [:maybe ms/JSONString]} (process-query-for-card-with-public-uuid uuid :api (json/decode+kw parameters) :qp qp.pivot/run-pivot-query)) |
/pivot/dashboard/:uuid/dashcard/:dashcard-id/card/:card-id | (api/defendpoint GET "Fetch the results for a Card in a publicly-accessible Dashboard. Does not require auth credentials. Public sharing must be enabled." [uuid card-id dashcard-id parameters] {uuid ms/UUIDString card-id ms/PositiveInt dashcard-id ms/PositiveInt parameters [:maybe ms/JSONString]} (validation/check-public-sharing-enabled) (api/check-404 (t2/select-one-pk :model/Card :id card-id :archived false)) (let [dashboard-id (api/check-404 (t2/select-one-pk Dashboard :public_uuid uuid, :archived false))] (u/prog1 (process-query-for-dashcard :dashboard-id dashboard-id :card-id card-id :dashcard-id dashcard-id :export-format :api :parameters parameters :qp qp.pivot/run-pivot-query) (events/publish-event! :event/card-read {:object-id card-id, :user-id api/*current-user-id*, :context :dashboard})))) |
Rate limit at 10 actions per 1000 ms on a per action basis. The goal of rate limiting should be to prevent very obvious abuse, but it should be relatively lax so we don't annoy legitimate users. | (def ^:private action-execution-throttle (throttle/make-throttler :action-uuid :attempts-threshold 10 :initial-delay-ms 1000 :attempt-ttl-ms 1000 :delay-exponent 1)) |
/action/:uuid/execute | (api/defendpoint POST "Execute the Action. `parameters` should be the mapped dashboard parameters with values." [uuid :as {{:keys [parameters], :as _body} :body}] {uuid ms/UUIDString parameters [:maybe [:map-of :keyword any?]]} (let [throttle-message (try (throttle/check action-execution-throttle uuid) nil (catch ExceptionInfo e (get-in (ex-data e) [:errors :action-uuid]))) throttle-time (when throttle-message (second (re-find #"You must wait ([0-9]+) seconds" throttle-message)))] (if throttle-message (cond-> {:status 429 :body throttle-message} throttle-time (assoc :headers {"Retry-After" throttle-time})) (do (validation/check-public-sharing-enabled) ;; Run this query with full superuser perms. We don't want the various perms checks ;; failing because there are no current user perms; if this Dashcard is public ;; you're by definition allowed to run it without a perms check anyway (request/as-admin (let [action (api/check-404 (action/select-action :public_uuid uuid :archived false))] (snowplow/track-event! ::snowplow/action {:event :action-executed :source :public_form :type (:type action) :action_id (:id action)}) ;; Undo middleware string->keyword coercion (actions/execute-action! action (update-keys parameters name)))))))) |
----------------------------------------- Route Definitions & Complaints ----------------------------------------- | |
TODO - why don't we just make these routes have a bit of middleware that includes the
TODO - also a smart person would probably just parse the UUIDs automatically in middleware as appropriate for
| (api/define-routes) |