2018-07-20 10:22:46 +03:00
|
|
|
{-# LANGUAGE DataKinds #-}
|
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
|
|
{-# LANGUAGE LambdaCase #-}
|
|
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2018-08-03 11:43:35 +03:00
|
|
|
{-# LANGUAGE RankNTypes #-}
|
2018-07-20 10:22:46 +03:00
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
|
|
|
|
module Hasura.Server.Auth
|
|
|
|
( getUserInfo
|
|
|
|
, AuthMode(..)
|
|
|
|
) where
|
|
|
|
|
|
|
|
import Control.Exception (try)
|
|
|
|
import Control.Lens
|
|
|
|
import Data.Aeson
|
|
|
|
import Data.CaseInsensitive (CI (..), original)
|
|
|
|
|
|
|
|
import qualified Data.ByteString as B
|
2018-08-03 11:43:35 +03:00
|
|
|
import qualified Data.ByteString.Lazy as BL
|
2018-07-20 10:22:46 +03:00
|
|
|
import qualified Data.HashMap.Strict as M
|
|
|
|
import qualified Data.Text as T
|
|
|
|
import qualified Data.Text.Encoding as TE
|
|
|
|
import qualified Data.Text.Encoding.Error as TE
|
|
|
|
import qualified Network.HTTP.Client as H
|
|
|
|
import qualified Network.HTTP.Types as N
|
|
|
|
import qualified Network.Wreq as Wreq
|
|
|
|
|
|
|
|
import Hasura.Prelude
|
|
|
|
import Hasura.RQL.Types
|
2018-08-03 11:43:35 +03:00
|
|
|
import Hasura.Server.Logging
|
|
|
|
|
|
|
|
import qualified Hasura.Logging as L
|
2018-07-20 10:22:46 +03:00
|
|
|
|
|
|
|
bsToTxt :: B.ByteString -> T.Text
|
|
|
|
bsToTxt = TE.decodeUtf8With TE.lenientDecode
|
|
|
|
|
|
|
|
data AuthMode
|
|
|
|
= AMNoAuth
|
|
|
|
| AMAccessKey !T.Text
|
|
|
|
| AMAccessKeyAndHook !T.Text !T.Text
|
|
|
|
deriving (Show, Eq)
|
|
|
|
|
2018-08-03 11:43:35 +03:00
|
|
|
type WebHookLogger = WebHookLog -> IO ()
|
2018-07-20 10:22:46 +03:00
|
|
|
|
|
|
|
userRoleHeader :: T.Text
|
|
|
|
userRoleHeader = "x-hasura-role"
|
|
|
|
|
2018-08-03 11:43:35 +03:00
|
|
|
mkUserInfoFromResp
|
|
|
|
:: (MonadIO m, MonadError QErr m)
|
|
|
|
=> WebHookLogger
|
|
|
|
-> T.Text
|
|
|
|
-> N.Status
|
|
|
|
-> BL.ByteString
|
|
|
|
-> m UserInfo
|
|
|
|
mkUserInfoFromResp logger url statusCode respBody
|
|
|
|
| statusCode == N.status200 =
|
|
|
|
case eitherDecode respBody of
|
|
|
|
Left e -> do
|
|
|
|
logError
|
|
|
|
throw500 $ "Invalid response from authorization hook: " <> T.pack e
|
|
|
|
Right rawHeaders -> getUserInfoFromHdrs rawHeaders
|
|
|
|
|
|
|
|
| statusCode == N.status401 = do
|
|
|
|
logError
|
|
|
|
throw401 "Authentication hook unauthorized this request"
|
|
|
|
|
|
|
|
| otherwise = do
|
|
|
|
logError
|
|
|
|
throw500 "Invalid response from authorization hook"
|
|
|
|
where
|
|
|
|
getUserInfoFromHdrs rawHeaders = do
|
|
|
|
let headers = M.fromList [(T.toLower k, v) | (k, v) <- M.toList rawHeaders]
|
|
|
|
case M.lookup userRoleHeader headers of
|
|
|
|
Nothing -> do
|
|
|
|
logError
|
|
|
|
throw500 "missing x-hasura-role key in webhook response"
|
|
|
|
Just v -> do
|
|
|
|
logWebHookResp L.LevelInfo Nothing
|
|
|
|
return $ UserInfo (RoleName v) headers
|
|
|
|
|
|
|
|
logError =
|
|
|
|
logWebHookResp L.LevelError $ Just respBody
|
|
|
|
|
|
|
|
logWebHookResp logLevel mResp =
|
|
|
|
liftIO $ logger $ WebHookLog logLevel (Just statusCode)
|
|
|
|
url Nothing $ fmap (bsToTxt . BL.toStrict) mResp
|
|
|
|
|
2018-07-20 10:22:46 +03:00
|
|
|
userInfoFromWebhook
|
|
|
|
:: (MonadIO m, MonadError QErr m)
|
2018-08-03 11:43:35 +03:00
|
|
|
=> WebHookLogger
|
|
|
|
-> H.Manager
|
2018-07-20 10:22:46 +03:00
|
|
|
-> T.Text
|
|
|
|
-> [N.Header]
|
|
|
|
-> m UserInfo
|
2018-08-03 11:43:35 +03:00
|
|
|
userInfoFromWebhook logger manager urlT reqHeaders = do
|
2018-07-20 10:22:46 +03:00
|
|
|
let options =
|
|
|
|
Wreq.defaults
|
|
|
|
& Wreq.headers .~ filteredHeaders
|
|
|
|
& Wreq.checkResponse ?~ (\_ _ -> return ())
|
|
|
|
& Wreq.manager .~ Right manager
|
|
|
|
|
|
|
|
res <- liftIO $ try $ Wreq.getWith options $ T.unpack urlT
|
2018-08-03 11:43:35 +03:00
|
|
|
resp <- either logAndThrow return res
|
2018-07-20 10:22:46 +03:00
|
|
|
let status = resp ^. Wreq.responseStatus
|
2018-08-03 11:43:35 +03:00
|
|
|
respBody = resp ^. Wreq.responseBody
|
2018-07-20 10:22:46 +03:00
|
|
|
|
2018-08-03 11:43:35 +03:00
|
|
|
mkUserInfoFromResp logger urlT status respBody
|
2018-07-20 10:22:46 +03:00
|
|
|
where
|
2018-08-03 11:43:35 +03:00
|
|
|
logAndThrow err = do
|
|
|
|
liftIO $ logger $ WebHookLog L.LevelError Nothing urlT (Just err) Nothing
|
|
|
|
throw500 "Internal Server Error"
|
|
|
|
|
2018-07-20 10:22:46 +03:00
|
|
|
filteredHeaders = flip filter reqHeaders $ \(n, _) ->
|
2018-07-27 12:34:50 +03:00
|
|
|
n `notElem` ["Content-Length", "User-Agent", "Host", "Origin", "Referer"]
|
2018-07-20 10:22:46 +03:00
|
|
|
|
|
|
|
accessKeyHeader :: T.Text
|
|
|
|
accessKeyHeader = "x-hasura-access-key"
|
|
|
|
|
|
|
|
getUserInfo
|
|
|
|
:: (MonadIO m, MonadError QErr m)
|
2018-08-03 11:43:35 +03:00
|
|
|
=> WebHookLogger
|
|
|
|
-> H.Manager
|
2018-07-20 10:22:46 +03:00
|
|
|
-> [N.Header]
|
|
|
|
-> AuthMode
|
|
|
|
-> m UserInfo
|
2018-08-03 11:43:35 +03:00
|
|
|
getUserInfo logger manager rawHeaders = \case
|
2018-07-20 10:22:46 +03:00
|
|
|
|
|
|
|
AMNoAuth -> return userInfoFromHeaders
|
|
|
|
|
|
|
|
AMAccessKey accKey ->
|
|
|
|
case getHeader accessKeyHeader of
|
|
|
|
Just givenAccKey -> userInfoWhenAccessKey accKey givenAccKey
|
|
|
|
Nothing -> throw401 "x-hasura-access-key required, but not found"
|
|
|
|
|
|
|
|
AMAccessKeyAndHook accKey hook ->
|
|
|
|
maybe
|
2018-08-03 11:43:35 +03:00
|
|
|
(userInfoFromWebhook logger manager hook rawHeaders)
|
2018-07-20 10:22:46 +03:00
|
|
|
(userInfoWhenAccessKey accKey) $
|
|
|
|
getHeader accessKeyHeader
|
|
|
|
|
|
|
|
where
|
|
|
|
|
|
|
|
headers =
|
|
|
|
M.fromList $ filter (T.isPrefixOf "x-hasura-" . fst) $
|
|
|
|
flip map rawHeaders $
|
|
|
|
\(hdrName, hdrVal) ->
|
|
|
|
(T.toLower $ bsToTxt $ original hdrName, bsToTxt hdrVal)
|
|
|
|
|
|
|
|
getHeader h = M.lookup h headers
|
|
|
|
|
|
|
|
userInfoFromHeaders =
|
|
|
|
case M.lookup "x-hasura-role" headers of
|
|
|
|
Just v -> UserInfo (RoleName v) headers
|
|
|
|
Nothing -> UserInfo adminRole M.empty
|
|
|
|
|
|
|
|
userInfoWhenAccessKey key reqKey = do
|
|
|
|
when (reqKey /= key) $ throw401 "invalid x-hasura-access-key"
|
|
|
|
return userInfoFromHeaders
|