metabase MB_API_KEY不生效及ring handler简易分析

分析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*)))})

猜你喜欢

转载自blog.csdn.net/weixin_40455124/article/details/114546309