分析handler的起因是测试MB_API_KEY发现不生效:
官方文档描述如下:
MB_API_KEY
Type: string
Default: null
Middleware that enforces validation of the client via the request header X-Metabase-Apikey. If the header is available, then it’s validated against MB_API_KEY. When it matches, the request continues; otherwise it’s blocked with a 403 Forbidden response.
测试发现设置后访问API是和没有设置一样的。
开始分析handler,从project.clj
:ring
{
:handler metabase.handler/app
:init metabase.core/init!
:async? true
:destroy metabase.core/destroy
:reload-paths ["src"]}}]
app 的代码如下:
(def app
"The primary entry point to the Ring HTTP server."
(->
;; in production, dereference routes now because they will not change at runtime, so we don't need to waste time
;; dereferencing the var on every request. For dev & test, use the var instead so it can be tweaked without having
;; to restart the web server
(if config/is-prod?
routes/routes
#'routes/routes)
;; ▼▼▼ POST-PROCESSING ▼▼▼ happens from TOP-TO-BOTTOM
mw.exceptions/catch-uncaught-exceptions ; catch any Exceptions that weren't passed to `raise`
mw.exceptions/catch-api-exceptions ; catch exceptions and return them in our expected format
mw.log/log-api-call
mw.security/add-security-headers ; Add HTTP headers to API responses to prevent them from being cached
mw.json/wrap-json-body ; extracts json POST body and makes it avaliable on request
mw.json/wrap-streamed-json-response ; middleware to automatically serialize suitable objects as JSON in responses
wrap-keyword-params ; converts string keys in :params to keyword keys
wrap-params ; parses GET and POST params as :query-params/:form-params and both as :params
mw.misc/maybe-set-site-url ; set the value of `site-url` if it hasn't been set yet
mw.session/bind-current-user ; Binds *current-user* and *current-user-id* if :metabase-user-id is non-nil
mw.session/wrap-current-user-info ; looks for :metabase-session-id and sets :metabase-user-id and other info if Session ID is valid
mw.session/wrap-session-id ; looks for a Metabase Session ID and assoc as :metabase-session-id
mw.auth/wrap-api-key ; looks for a Metabase API Key on the request and assocs as :metabase-api-key
wrap-cookies ; Parses cookies in the request map and assocs as :cookies
mw.misc/add-content-type ; Adds a Content-Type header for any response that doesn't already have one
mw.misc/disable-streaming-buffering ; Add header to streaming (async) responses so ngnix doesn't buffer keepalive bytes
wrap-gzip ; GZIP response if client can handle it
mw.misc/bind-request ; bind `metabase.middleware.misc/*request*` for the duration of the request
mw.ssl/redirect-to-https-middleware)) ; Redirect to HTTPS if configured to do so
;; ▲▲▲ PRE-PROCESSING ▲▲▲ happens from BOTTOM-TO-TOP
这里不能被mw.auth/wrap-api-key迷惑,这个mw.auth/wrap-api-key代码如下:
(defn- wrap-api-key* [{
:keys [headers], :as request}]
(if-let [api-key (headers metabase-api-key-header)]
(assoc request :metabase-api-key api-key)
request))
(defn wrap-api-key
"Middleware that sets the `:metabase-api-key` keyword on the request if a valid API Key can be found. We check the
request headers for `X-METABASE-APIKEY` and if it's not found then then no keyword is bound to the request."
[handler]
(fn [request respond raise]
(handler (wrap-api-key* request) respond raise)))
这个middleware 其实只是设置了一个key,没有进行效验。
那么enforce在哪里呢,从
(if config/is-prod?
routes/routes
#'routes/routes)
分析对应代码
(defroutes ^{
:doc "Top-level ring routes for Metabase."} routes
(or (some-> (resolve 'ee.sso.routes/routes) var-get)
(fn [_ respond _]
(respond nil)))
;; ^/$ -> index.html
(GET "/" [] index/index)
(GET "/favicon.ico" [] (resp/resource-response (public-settings/application-favicon-url)))
;; ^/api/health -> Health Check Endpoint
(GET "/api/health" [] (if (init-status/complete?)
{
:status 200, :body {
:status "ok"}}
{
:status 503, :body {
:status "initializing", :progress (init-status/progress)}}))
;; ^/api/ -> All other API routes
(context "/api" [] (fn [& args]
;; Redirect naughty users who try to visit a page other than setup if setup is not yet complete
;;
;; if Metabase is not finished initializing, return a generic error message rather than
;; something potentially confusing like "DB is not set up"
(if-not (init-status/complete?)
{
:status 503, :body "Metabase is still initializing. Please sit tight..."}
(apply api/routes args))))
;; ^/app/ -> static files under frontend_client/app
(context "/app" []
(route/resources "/" {
:root "frontend_client/app"})
;; return 404 for anything else starting with ^/app/ that doesn't exist
(route/not-found {
:status 404, :body "Not found."}))
;; ^/public/ -> Public frontend and download routes
(context "/public" [] public-routes)
;; ^/emebed/ -> Embed frontend and download routes
(context "/embed" [] embed-routes)
;; Anything else (e.g. /user/edit_current) should serve up index.html; React app will handle the rest
(GET "*" [] index/index))
api handler里面的(apply api/routes args)如下:
(defroutes ^{
:doc "Ring routes for API endpoints."} routes
(or (some-> (resolve 'ee.sandbox.routes/routes) var-get)
(fn [_ respond _]
(respond nil)))
(context "/activity" [] (+auth activity/routes))
(context "/alert" [] (+auth alert/routes))
(context "/automagic-dashboards" [] (+auth magic/routes))
(context "/card" [] (+auth card/routes))
(context "/collection" [] (+auth collection/routes))
(context "/dashboard" [] (+auth dashboard/routes))
(context "/database" [] (+auth database/routes))
(context "/dataset" [] (+auth dataset/routes))
(context "/email" [] (+auth email/routes))
(context "/embed" [] (+message-only-exceptions embed/routes))
(context "/field" [] (+auth field/routes))
(context "/geojson" [] geojson/routes)
(context "/ldap" [] (+auth ldap/routes))
(context "/metastore" [] (+auth metastore/routes))
(context "/metric" [] (+auth metric/routes))
(context "/native-query-snippet" [] (+auth native-query-snippet/routes))
(context "/notify" [] (+apikey notify/routes))
(context "/permissions" [] (+auth permissions/routes))
(context "/preview_embed" [] (+auth preview-embed/routes))
(context "/public" [] (+generic-exceptions public/routes))
(context "/pulse" [] (+auth pulse/routes))
(context "/revision" [] (+auth revision/routes))
(context "/search" [] (+auth search/routes))
(context "/segment" [] (+auth segment/routes))
(context "/session" [] session/routes)
(context "/setting" [] (+auth setting/routes))
(context "/setup" [] setup/routes)
(context "/slack" [] (+auth slack/routes))
(context "/table" [] (+auth table/routes))
(context "/task" [] (+auth task/routes))
(context "/testing" [] (if (or config/is-dev?
(config/config-bool :mb-enable-test-endpoints))
testing/routes
(fn [_ respond _] (respond nil))))
(context "/tiles" [] (+auth tiles/routes))
(context "/transform" [] (+auth transform/routes))
(context "/user" [] (+auth user/routes))
(context "/util" [] util/routes)
(route/not-found (constantly {
:status 404, :body (deferred-tru "API endpoint does not exist.")})))
可以发现只有一个apikey ,大部分都是+auth,由此可以得出apikey 其实没有使用,apikey和auth对应代码如下:
(def ^:private +apikey
"Wrap `routes` so they may only be accessed with a correct API key header."
middleware.auth/enforce-api-key)
(def ^:private +auth
"Wrap `routes` so they may only be accessed with proper authentication credentials."
middleware.auth/enforce-authentication)
附加进行了取消+auth测试,如果不发送下面的metabase.SESSION
curl 'http://192.168.157.140:3000/api/card/2' \
-H 'Connection: keep-alive' \
-H 'Cache-Control: max-age=0' \
-H 'Upgrade-Insecure-Requests: 1' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \
-H 'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7' \
-H 'Cookie: _ga=GA1.1.603681071.1595490481; metabase.SEEN_ALERT_SPLASH=true; metabase.SESSION=f7fa2a82-e552-4ced-ab43-29fcdf0da183' \
--compressed \
--insecure
会发现有如下报错:
2021-03-07 16:57:56,903 DEBUG middleware.log :: GET /api/card/2 200 1.4 s (6 DB calls) App DB connections: 0/4 Jetty threads: 4/50 (3 idle, 0 queued) (43 total active threads) Queries in flight: 0 (0 queued)
2021-03-07 16:58:12,469 ERROR middleware.log :: GET /api/card/2 500 47.9 ms (3 DB calls)
{
:value [nil #{
"/collection/root/"}],
:error [(named (not (set? nil)) permissions-set) nil],
:via
[{
:type clojure.lang.ExceptionInfo,
:message "Input to set-has-full-permissions-for-set? does not match schema: \n\n\t [(named (not (set? nil)) permissions-set) nil] \n\n",
:data
{
:type :schema.core/error,
:schema
[#schema.core.One{
:schema #{
(pred "Valid user permissions path.")}, :optional? false, :name permissions-set}
#schema.core.One{
:schema #{
(pred "Valid permissions object path.")}, :optional? false, :name object-paths-set}],
:value [nil #{
"/collection/root/"}],
:error [(named (not (set? nil)) permissions-set) nil]},
:at [metabase.models.permissions$eval39311$set_has_full_permissions_for_set_QMARK___39316 invoke "permissions.clj" 272]}],
:trace
这个报错主要的有以下几个原因:
1、card查询过程需要有对应用户的权限等信息,没有session就找不到这些信息
2、很多api有 配置check-superuser,这个也需要有userid
如下为一个代码例子
(api/defendpoint POST "/:card-id/public_link"
"Generate publicly-accessible links for this Card. Returns UUID to be used in public links. (If this Card has
already been shared, it will return the existing public link rather than creating a new one.) Public sharing must
be enabled."
[card-id]
(api/check-superuser)
(api/check-public-sharing-enabled)
(api/check-not-archived (api/read-check Card card-id))
{
:uuid (or (db/select-one-field :public_uuid Card :id card-id)
(u/prog1 (str (UUID/randomUUID))
(db/update! Card card-id
:public_uuid <>
:made_public_by_id api/*current-user-id*)))})