mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 12:31:52 +03:00
342391f39d
This upgrades the version of Ormolu required by the HGE repository to v0.5.0.1, and reformats all code accordingly. Ormolu v0.5 reformats code that uses infix operators. This is mostly useful, adding newlines and indentation to make it clear which operators are applied first, but in some cases, it's unpleasant. To make this easier on the eyes, I had to do the following: * Add a few fixity declarations (search for `infix`) * Add parentheses to make precedence clear, allowing Ormolu to keep everything on one line * Rename `relevantEq` to `(==~)` in #6651 and set it to `infix 4` * Add a few _.ormolu_ files (thanks to @hallettj for helping me get started), mostly for Autodocodec operators that don't have explicit fixity declarations In general, I think these changes are quite reasonable. They mostly affect indentation. PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6675 GitOrigin-RevId: cd47d87f1d089fb0bc9dcbbe7798dbceedcd7d83
158 lines
5.5 KiB
Haskell
158 lines
5.5 KiB
Haskell
module Hasura.Server.Auth.WebHook
|
|
( AuthHookType (..),
|
|
AuthHook (..),
|
|
userInfoFromAuthHook,
|
|
)
|
|
where
|
|
|
|
import Control.Exception.Lifted (try)
|
|
import Control.Lens
|
|
import Control.Monad.Trans.Control (MonadBaseControl)
|
|
import Data.Aeson
|
|
import Data.Aeson qualified as J
|
|
import Data.ByteString.Lazy qualified as BL
|
|
import Data.HashMap.Strict qualified as Map
|
|
import Data.Parser.CacheControl (parseMaxAge)
|
|
import Data.Parser.Expires
|
|
import Data.Text qualified as T
|
|
import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime)
|
|
import Hasura.Base.Error
|
|
import Hasura.GraphQL.Transport.HTTP.Protocol qualified as GH
|
|
import Hasura.HTTP
|
|
import Hasura.Logging
|
|
import Hasura.Prelude
|
|
import Hasura.Server.Logging
|
|
import Hasura.Server.Utils
|
|
import Hasura.Session
|
|
import Hasura.Tracing qualified as Tracing
|
|
import Network.HTTP.Client.Transformable qualified as HTTP
|
|
import Network.Wreq qualified as Wreq
|
|
|
|
data AuthHookType
|
|
= AHTGet
|
|
| AHTPost
|
|
deriving (Eq)
|
|
|
|
instance Show AuthHookType where
|
|
show AHTGet = "GET"
|
|
show AHTPost = "POST"
|
|
|
|
data AuthHook = AuthHook
|
|
{ ahUrl :: Text,
|
|
ahType :: AuthHookType
|
|
}
|
|
deriving (Show, Eq)
|
|
|
|
hookMethod :: AuthHook -> HTTP.StdMethod
|
|
hookMethod authHook = case ahType authHook of
|
|
AHTGet -> HTTP.GET
|
|
AHTPost -> HTTP.POST
|
|
|
|
-- | Makes an authentication request to the given AuthHook and returns
|
|
-- UserInfo parsed from the response, plus an expiration time if one
|
|
-- was returned. Optionally passes a batch of raw GraphQL requests
|
|
-- for finer-grained auth. (#2666)
|
|
userInfoFromAuthHook ::
|
|
forall m.
|
|
(MonadIO m, MonadBaseControl IO m, MonadError QErr m, Tracing.MonadTrace m) =>
|
|
Logger Hasura ->
|
|
HTTP.Manager ->
|
|
AuthHook ->
|
|
[HTTP.Header] ->
|
|
Maybe GH.ReqsText ->
|
|
m (UserInfo, Maybe UTCTime, [HTTP.Header])
|
|
userInfoFromAuthHook logger manager hook reqHeaders reqs = do
|
|
resp <- (`onLeft` logAndThrow) =<< try performHTTPRequest
|
|
let status = resp ^. Wreq.responseStatus
|
|
respBody = resp ^. Wreq.responseBody
|
|
cookieHeaders = filter (\(headerName, _) -> headerName == "Set-Cookie") (resp ^. Wreq.responseHeaders)
|
|
|
|
mkUserInfoFromResp logger (ahUrl hook) (hookMethod hook) status respBody cookieHeaders
|
|
where
|
|
performHTTPRequest :: m (Wreq.Response BL.ByteString)
|
|
performHTTPRequest = do
|
|
let url = T.unpack $ ahUrl hook
|
|
req <- liftIO $ HTTP.mkRequestThrow $ T.pack url
|
|
Tracing.tracedHttpRequest req \req' -> liftIO do
|
|
case ahType hook of
|
|
AHTGet -> do
|
|
let isCommonHeader = (`elem` commonClientHeadersIgnored)
|
|
filteredHeaders = filter (not . isCommonHeader . fst) reqHeaders
|
|
req'' = req' & set HTTP.headers (addDefaultHeaders filteredHeaders)
|
|
HTTP.performRequest req'' manager
|
|
AHTPost -> do
|
|
let contentType = ("Content-Type", "application/json")
|
|
headersPayload = J.toJSON $ Map.fromList $ hdrsToText reqHeaders
|
|
req'' =
|
|
req
|
|
& set HTTP.method "POST"
|
|
& set HTTP.headers (addDefaultHeaders [contentType])
|
|
& set HTTP.body (Just $ J.encode $ object ["headers" J..= headersPayload, "request" J..= reqs])
|
|
HTTP.performRequest req'' manager
|
|
|
|
logAndThrow :: HTTP.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 ->
|
|
Text ->
|
|
HTTP.StdMethod ->
|
|
HTTP.Status ->
|
|
BL.ByteString ->
|
|
[HTTP.Header] ->
|
|
m (UserInfo, Maybe UTCTime, [HTTP.Header])
|
|
mkUserInfoFromResp (Logger logger) url method statusCode respBody respHdrs
|
|
| statusCode == HTTP.status200 =
|
|
case eitherDecode respBody of
|
|
Left e -> do
|
|
logError
|
|
throw500 $ "Invalid response from authorization hook: " <> T.pack e
|
|
Right rawHeaders -> getUserInfoFromHdrs rawHeaders respHdrs
|
|
| statusCode == HTTP.status401 = do
|
|
logError
|
|
throw401 "Authentication hook unauthorized this request"
|
|
| otherwise = do
|
|
logError
|
|
throw500 "Invalid response from authorization hook"
|
|
where
|
|
getUserInfoFromHdrs rawHeaders responseHdrs = do
|
|
userInfo <-
|
|
mkUserInfo URBFromSessionVariables UAdminSecretNotSent $
|
|
mkSessionVariablesText rawHeaders
|
|
logWebHookResp LevelInfo Nothing Nothing
|
|
expiration <- runMaybeT $ timeFromCacheControl rawHeaders <|> timeFromExpires rawHeaders
|
|
pure (userInfo, expiration, responseHdrs)
|
|
|
|
logWebHookResp :: MonadIO m => LogLevel -> Maybe BL.ByteString -> Maybe 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
|