graphql-engine/server/src-lib/Hasura/Server/Auth/WebHook.hs
Phil Freeman 0dddbe9e9d
Add MonadTrace and MonadExecuteQuery abstractions (#5383)
Co-authored-by: Vamshi Surabhi <0x777@users.noreply.github.com>
2020-07-15 16:10:48 +05:30

148 lines
5.3 KiB
Haskell

module Hasura.Server.Auth.WebHook
( AuthHookType(..)
, AuthHookG (..)
, AuthHook
, userInfoFromAuthHook
) where
import Control.Exception.Lifted (try)
import Control.Lens
import Control.Monad.Trans.Control (MonadBaseControl)
import Control.Monad.Trans.Maybe
import Data.Aeson
import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)
import Hasura.Server.Version (HasVersion)
import qualified Data.Aeson as J
import qualified Data.ByteString.Lazy as BL
import qualified Data.HashMap.Strict as Map
import qualified Data.Text as T
import qualified Network.HTTP.Client as H
import qualified Network.HTTP.Types as N
import qualified Network.Wreq as Wreq
import Data.Parser.CacheControl
import Data.Parser.Expires
import Hasura.HTTP
import Hasura.Logging
import Hasura.Prelude
import Hasura.RQL.Types
import Hasura.Server.Logging
import Hasura.Server.Utils
import Hasura.Session
import qualified Hasura.Tracing as Tracing
data AuthHookType
= AHTGet
| AHTPost
deriving (Eq)
instance Show AuthHookType where
show AHTGet = "GET"
show AHTPost = "POST"
data AuthHookG a b
= AuthHookG
{ ahUrl :: !a
, ahType :: !b
} deriving (Show, Eq)
type AuthHook = AuthHookG T.Text AuthHookType
hookMethod :: AuthHook -> N.StdMethod
hookMethod authHook = case ahType authHook of
AHTGet -> N.GET
AHTPost -> N.POST
-- | Makes an authentication request to the given AuthHook and returns
-- UserInfo parsed from the response, plus an expiration time if one
-- was returned.
userInfoFromAuthHook
:: forall m
. (HasVersion, MonadIO m, MonadBaseControl IO m, MonadError QErr m, Tracing.MonadTrace m)
=> Logger Hasura
-> H.Manager
-> AuthHook
-> [N.Header]
-> m (UserInfo, Maybe UTCTime)
userInfoFromAuthHook logger manager hook reqHeaders = do
resp <- (`onLeft` logAndThrow) =<< try performHTTPRequest
let status = resp ^. Wreq.responseStatus
respBody = resp ^. Wreq.responseBody
mkUserInfoFromResp logger (ahUrl hook) (hookMethod hook) status respBody
where
performHTTPRequest :: m (Wreq.Response BL.ByteString)
performHTTPRequest = Tracing.traceHttpRequest (ahUrl hook) do
let url = T.unpack $ ahUrl hook
req <- liftIO $ H.parseRequest url
pure $ Tracing.SuspendedRequest req \req' -> liftIO do
case ahType hook of
AHTGet -> do
let isCommonHeader = (`elem` commonClientHeadersIgnored)
filteredHeaders = filter (not . isCommonHeader . fst) reqHeaders
H.httpLbs (req' { H.requestHeaders = addDefaultHeaders filteredHeaders }) manager
AHTPost -> do
let contentType = ("Content-Type", "application/json")
headersPayload = J.toJSON $ Map.fromList $ hdrsToText reqHeaders
H.httpLbs (req' { H.method = "POST"
, H.requestHeaders = addDefaultHeaders [contentType]
, H.requestBody = H.RequestBodyLBS . J.encode $ object ["headers" J..= headersPayload]
}) manager
logAndThrow :: H.HttpException -> m a
logAndThrow err = do
unLogger logger $
WebHookLog LevelError Nothing (ahUrl hook) (hookMethod hook)
(Just $ HttpException err) Nothing Nothing
throw500 "webhook authentication request failed"
mkUserInfoFromResp
:: (MonadIO m, MonadError QErr m)
=> Logger Hasura
-> T.Text
-> N.StdMethod
-> N.Status
-> BL.ByteString
-> m (UserInfo, Maybe UTCTime)
mkUserInfoFromResp (Logger logger) url method 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
userInfo <- mkUserInfo URBFromSessionVariables UAdminSecretNotSent $
mkSessionVariablesText $ Map.toList rawHeaders
logWebHookResp LevelInfo Nothing Nothing
expiration <- runMaybeT $ timeFromCacheControl rawHeaders <|> timeFromExpires rawHeaders
pure (userInfo, expiration)
logWebHookResp :: MonadIO m => LogLevel -> Maybe BL.ByteString -> Maybe T.Text -> m ()
logWebHookResp logLevel mResp message =
logger $ WebHookLog logLevel (Just statusCode)
url method Nothing (bsToTxt . BL.toStrict <$> mResp) message
logWarn message = logWebHookResp LevelWarn (Just respBody) (Just message)
logError = logWebHookResp LevelError (Just respBody) Nothing
timeFromCacheControl headers = do
header <- afold $ Map.lookup "Cache-Control" headers
duration <- parseMaxAge header `onLeft` \err -> logWarn (T.pack err) *> empty
addUTCTime (fromInteger duration) <$> liftIO getCurrentTime
timeFromExpires headers = do
header <- afold $ Map.lookup "Expires" headers
parseExpirationTime header `onLeft` \err -> logWarn (T.pack err) *> empty