server: webhook auth token caching

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7925
Co-authored-by: Sean Park-Ross <94021366+seanparkross@users.noreply.github.com>
GitOrigin-RevId: eae1f4023a9e9144c9eb230529c214cb4327e44f
This commit is contained in:
paritosh-08 2023-03-14 23:57:21 +05:30 committed by hasura-bot
parent 88488362e0
commit b36971f637
11 changed files with 138 additions and 13 deletions

View File

@ -213,6 +213,59 @@ fields and a new websocket connection is established.
:::
## Caching webhook session variables
<div className="badge badge--primary heading-badge">
Available on: Cloud Standard, Cloud Professional, Cloud Enterprise, Self-hosted Enterprise
</div>
Webhook session variables can be cached in order to improve performance of the request. For caching, you need to
return either:
- a `Cache-Control` variable, modeled on the
[Cache-Control HTTP Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control), to specify a
**relative** expiration time, in seconds.
```http
HTTP/1.1 200 OK
Content-Type: application/json
{
"X-Hasura-User-Id": "26",
"X-Hasura-Role": "user",
"X-Hasura-Is-Owner": "false",
"Cache-Control": "max-age=60"
}
```
- an `Expires` variable, modeled on the
[Expires HTTP Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires), to specify an **absolute**
expiration time. The expected format is `"%a, %d %b %Y %T GMT"`.
```http
HTTP/1.1 200 OK
Content-Type: application/json
{
"X-Hasura-User-Id": "27",
"X-Hasura-Role": "user",
"X-Hasura-Is-Owner": "false",
"Expires": "Mon, 30 Mar 2020 13:25:18 GMT"
}
```
:::tip Tip
The cache key is based on the following parameters:
- Client headers
- Graphql request
This means that the cache key will change if the graphql request changes. If you want to cache auth token based on
client headers only, you can [omit the auth-hook request
body](deployment/graphql-engine-flags/reference.mdx#send-request-body-to-auth-hook).
:::
## Auth webhook samples
We have put together a

View File

@ -177,6 +177,19 @@ requests.
| **Default** | `GET` |
| **Supported in** | CE, EE, Cloud |
### Send Request Body to Auth Hook
Whether or not to send the request body (graphql request/variables) to the auth hook in `POST` mode.
| | |
| ------------------- | ----------------------------------------------- |
| **Flag** | `--auth-hook-send-request-body <true-or-false>` |
| **Env var** | `HASURA_GRAPHQL_AUTH_HOOK_SEND_REQUEST_BODY` |
| **Accepted values** | Boolean |
| **Options** | `true` or `false` |
| **Default** | `true` |
| **Supported in** | CE, EE, Cloud |
### BigQuery String Numeric Input
Stringify certain

View File

@ -2,6 +2,7 @@ module Hasura.HTTP
( wreqOptions,
HttpException (..),
hdrsToText,
textToHdrs,
addDefaultHeaders,
defaultHeaders,
HttpResponse (..),
@ -18,7 +19,7 @@ import Control.Exception (Exception (..), fromException)
import Control.Lens hiding ((.=))
import Data.Aeson qualified as J
import Data.Aeson.KeyMap qualified as KM
import Data.CaseInsensitive (original)
import Data.CaseInsensitive (mk, original)
import Data.HashMap.Strict qualified as M
import Data.Text qualified as T
import Data.Text.Conversions (UTF8 (..), convertText)
@ -40,6 +41,12 @@ hdrsToText hdrs =
| (hdrName, hdrVal) <- hdrs
]
textToHdrs :: [(Text, Text)] -> [HTTP.Header]
textToHdrs hdrs =
[ (mk (txtToBs hdrName), TE.encodeUtf8 hdrVal)
| (hdrName, hdrVal) <- hdrs
]
wreqOptions :: HTTP.Manager -> [HTTP.Header] -> Wreq.Options
wreqOptions manager hdrs =
Wreq.defaults

View File

@ -38,7 +38,9 @@ instance Show AuthHookType where
data AuthHook = AuthHook
{ ahUrl :: Text,
ahType :: AuthHookType
ahType :: AuthHookType,
-- | Whether to send the request body to the auth hook
ahSendRequestBody :: Bool
}
deriving (Show, Eq)
@ -86,7 +88,16 @@ userInfoFromAuthHook logger manager hook reqHeaders reqs = do
req
& set HTTP.method "POST"
& set HTTP.headers (addDefaultHeaders [contentType])
& set HTTP.body (Just $ J.encode $ object ["headers" J..= headersPayload, "request" J..= reqs])
& set
HTTP.body
( Just $
J.encode $
object
( ["headers" J..= headersPayload]
-- We will only send the request if `ahSendRequestBody` is set to true
<> ["request" J..= reqs | ahSendRequestBody hook]
)
)
HTTP.performRequest req' manager
logAndThrow :: HTTP.HttpException -> m a

View File

@ -236,7 +236,7 @@ mkConnParams ConnParamsRaw {..} = do
-- | Fetch 'Auth.AuthHook' components from the environment and merge
-- with the values consumed by the arg parser in 'AuthHookRaw'.
mkAuthHook :: Monad m => AuthHookRaw -> WithEnvT m (Maybe Auth.AuthHook)
mkAuthHook (AuthHookRaw mUrl mType) = do
mkAuthHook (AuthHookRaw mUrl mType mSendRequestBody) = do
mUrlEnv <- withOption mUrl authHookOption
-- Also support HASURA_GRAPHQL_AUTH_HOOK_TYPE
-- TODO (from master):- drop this in next major update <--- (NOTE: This comment is from 2020-08-21)
@ -247,7 +247,18 @@ mkAuthHook (AuthHookRaw mUrl mType) = do
<$> considerEnvs
[_envVar authHookModeOption, "HASURA_GRAPHQL_AUTH_HOOK_TYPE"]
)
pure $ (`Auth.AuthHook` authMode) <$> mUrlEnv
-- if authMode is `GET` then authSendRequestBody is set to `False`, otherwise we check for the config value
authSendRequestBody <-
case authMode of
Auth.AHTGet -> pure False
Auth.AHTPost ->
onNothing
mSendRequestBody
( fromMaybe (_default authHookSendRequestBodyOption)
<$> considerEnvs
[_envVar authHookSendRequestBodyOption]
)
pure $ (\url -> Auth.AuthHook url authMode authSendRequestBody) <$> mUrlEnv
-- | Fetch 'Cors.CorsConfig' settings from the environment and merge
-- with the settings consumed by the arg parser.

View File

@ -19,6 +19,7 @@ module Hasura.Server.Init.Arg.Command.Serve
accessKeyOption,
authHookOption,
authHookModeOption,
authHookSendRequestBodyOption,
jwtSecretOption,
unAuthRoleOption,
corsDomainOption,
@ -360,7 +361,7 @@ accessKeyOption =
parseAuthHook :: Opt.Parser Config.AuthHookRaw
parseAuthHook =
Config.AuthHookRaw <$> url <*> urlType
Config.AuthHookRaw <$> url <*> urlType <*> sendRequestBody
where
url =
Opt.optional $
@ -377,6 +378,14 @@ parseAuthHook =
<> Opt.metavar "<GET|POST>"
<> Opt.help (Config._helpMessage authHookModeOption)
)
sendRequestBody :: Opt.Parser (Maybe Bool) =
Opt.optional $
Opt.option
(Opt.eitherReader Env.fromEnv)
( Opt.long "auth-hook-send-request-body"
<> Opt.metavar "<true|false>"
<> Opt.help (Config._helpMessage authHookSendRequestBodyOption)
)
authHookOption :: Config.Option ()
authHookOption =
@ -394,6 +403,14 @@ authHookModeOption =
Config._helpMessage = "HTTP method to use for authorization webhook (default: GET)"
}
authHookSendRequestBodyOption :: Config.Option Bool
authHookSendRequestBodyOption =
Config.Option
{ Config._default = True,
Config._envVar = "HASURA_GRAPHQL_AUTH_HOOK_SEND_REQUEST_BODY",
Config._helpMessage = "Send request body in POST method (default: true)"
}
parseJwtSecret :: Opt.Parser (Maybe Auth.JWTConfig)
parseJwtSecret =
Opt.optional $
@ -1183,6 +1200,7 @@ serveCmdFooter =
Config.optionPP accessKeyOption,
Config.optionPP authHookOption,
Config.optionPP authHookModeOption,
Config.optionPP authHookSendRequestBodyOption,
Config.optionPP jwtSecretOption,
Config.optionPP unAuthRoleOption,
Config.optionPP corsDomainOption,

View File

@ -498,7 +498,8 @@ instance Hashable API
data AuthHookRaw = AuthHookRaw
{ ahrUrl :: Maybe Text,
ahrType :: Maybe Auth.AuthHookType
ahrType :: Maybe Auth.AuthHookType,
ahrSendRequestBody :: Maybe Bool
}
-- | Sleep time interval for recurring activities such as (@'asyncActionsProcessor')

View File

@ -34,6 +34,7 @@ module Hasura.Server.Utils
useBackendOnlyPermissionsHeader,
userIdHeader,
userRoleHeader,
contentLengthHeader,
sessionVariablePrefix,
)
where
@ -92,6 +93,9 @@ userIdHeader = "x-hasura-user-id"
requestIdHeader :: IsString a => a
requestIdHeader = "x-request-id"
contentLengthHeader :: IsString a => a
contentLengthHeader = "Content-Length"
useBackendOnlyPermissionsHeader :: IsString a => a
useBackendOnlyPermissionsHeader = "x-hasura-use-backend-only-permissions"

View File

@ -1,3 +1,5 @@
{-# LANGUAGE TemplateHaskell #-}
module Hasura.Session
( RoleName,
mkRoleName,
@ -32,6 +34,7 @@ where
import Autodocodec (HasCodec (codec), dimapCodec)
import Data.Aeson
import Data.Aeson.TH qualified as J
import Data.Aeson.Types (Parser, toJSONKeyText)
import Data.CaseInsensitive qualified as CI
import Data.HashMap.Strict qualified as Map
@ -178,6 +181,8 @@ data BackendOnlyFieldAccess
| BOFADisallowed
deriving (Show, Eq, Generic)
$(J.deriveJSON hasuraJSON ''BackendOnlyFieldAccess)
instance Hashable BackendOnlyFieldAccess
data UserInfo = UserInfo
@ -189,6 +194,8 @@ data UserInfo = UserInfo
instance Hashable UserInfo
$(J.deriveJSON hasuraJSON ''UserInfo)
class (Monad m) => UserInfoM m where
askUserInfo :: m UserInfo

View File

@ -615,7 +615,7 @@ fakeJWTConfig =
in JWTConfig {..}
fakeAuthHook :: AuthHook
fakeAuthHook = AuthHook "http://fake" AHTGet
fakeAuthHook = AuthHook "http://fake" AHTGet False
mkRoleNameE :: Text -> RoleName
mkRoleNameE = fromMaybe (error "fixme") . mkRoleName

View File

@ -53,7 +53,7 @@ emptyServeOptionsRaw =
},
rsoTxIso = Nothing,
rsoAdminSecret = Nothing,
rsoAuthHook = UUT.AuthHookRaw Nothing Nothing,
rsoAuthHook = UUT.AuthHookRaw Nothing Nothing Nothing,
rsoJwtSecret = Nothing,
rsoUnAuthRole = Nothing,
rsoCorsConfig = Nothing,
@ -308,7 +308,7 @@ mkServeOptionsSpec =
-- Then
result = UUT.runWithEnv env (UUT.mkServeOptions @Hasura rawServeOptions)
fmap UUT.soAuthHook result `Hspec.shouldBe` Right (Just (Auth.AuthHook "http://auth.hook.com" Auth.AHTGet))
fmap UUT.soAuthHook result `Hspec.shouldBe` Right (Just (Auth.AuthHook "http://auth.hook.com" Auth.AHTGet False))
Hspec.it "Env > Nothing" $ do
let -- Given
@ -321,11 +321,11 @@ mkServeOptionsSpec =
-- Then
result = UUT.runWithEnv env (UUT.mkServeOptions @Hasura rawServeOptions)
fmap UUT.soAuthHook result `Hspec.shouldBe` Right (Just (Auth.AuthHook "http://auth.hook.com" Auth.AHTPost))
fmap UUT.soAuthHook result `Hspec.shouldBe` Right (Just (Auth.AuthHook "http://auth.hook.com" Auth.AHTPost True))
Hspec.it "Arg > Env" $ do
let -- Given
rawServeOptions = emptyServeOptionsRaw {UUT.rsoAuthHook = UUT.AuthHookRaw (Just "http://auth.hook.com") (Just Auth.AHTGet)}
rawServeOptions = emptyServeOptionsRaw {UUT.rsoAuthHook = UUT.AuthHookRaw (Just "http://auth.hook.com") (Just Auth.AHTGet) Nothing}
-- When
env =
[ (UUT._envVar UUT.authHookOption, "http://auth.hook.com"),
@ -334,7 +334,7 @@ mkServeOptionsSpec =
-- Then
result = UUT.runWithEnv env (UUT.mkServeOptions @Hasura rawServeOptions)
fmap UUT.soAuthHook result `Hspec.shouldBe` Right (Just (Auth.AuthHook "http://auth.hook.com" Auth.AHTGet))
fmap UUT.soAuthHook result `Hspec.shouldBe` Right (Just (Auth.AuthHook "http://auth.hook.com" Auth.AHTGet False))
Hspec.describe "soJwtSecret" $ do
Hspec.it "Env > Nothing" $ do