mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
parent
a002d3ad2a
commit
c19fe35f4e
@ -137,6 +137,14 @@ community tooling to write your own client-facing GraphQL gateway that interacts
|
||||
it out of the box** (*by as much as 4x*). If you need any help with remodeling these kind of use cases to use the
|
||||
built-in remote schemas feature, please get in touch with us on `Discord <https://discord.gg/vBPpJkS>`__.
|
||||
|
||||
Response headers from your remote GraphQL servers
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Response headers from your remote schema servers are sent back to the client
|
||||
over HTTP transport. **Over websocket transport, the response headers are not
|
||||
sent.** If you require the response headers from remote servers, use the HTTP
|
||||
transport.
|
||||
|
||||
|
||||
Bypassing Hasura's authorization system for remote schema queries
|
||||
-----------------------------------------------------------------
|
||||
|
||||
|
@ -142,6 +142,7 @@ library
|
||||
, Hasura.Server.Auth
|
||||
, Hasura.Server.Auth.JWT
|
||||
, Hasura.Server.Init
|
||||
, Hasura.Server.Context
|
||||
, Hasura.Server.Middleware
|
||||
, Hasura.Server.Logging
|
||||
, Hasura.Server.Query
|
||||
|
@ -27,6 +27,10 @@ import Hasura.GraphQL.Transport.HTTP.Protocol
|
||||
import Hasura.HTTP
|
||||
import Hasura.RQL.DDL.Headers
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Server.Context
|
||||
import Hasura.Server.Utils (bsToTxt,
|
||||
filterRequestHeaders,
|
||||
filterResponseHeaders)
|
||||
|
||||
import qualified Hasura.GraphQL.Resolve as R
|
||||
import qualified Hasura.GraphQL.Validate as VQ
|
||||
@ -42,7 +46,7 @@ runGQ
|
||||
-> [N.Header]
|
||||
-> GraphQLRequest
|
||||
-> BL.ByteString -- this can be removed when we have a pretty-printer
|
||||
-> m BL.ByteString
|
||||
-> m HResponse
|
||||
runGQ pool isoL userInfo sc manager reqHdrs req rawReq = do
|
||||
|
||||
(gCtx, _) <- flip runStateT sc $ getGCtx (userRole userInfo) gCtxRoleMap
|
||||
@ -104,7 +108,7 @@ runHasuraGQ
|
||||
-> UserInfo
|
||||
-> SchemaCache
|
||||
-> VQ.QueryParts
|
||||
-> m BL.ByteString
|
||||
-> m HResponse
|
||||
runHasuraGQ pool isoL userInfo sc queryParts = do
|
||||
(gCtx, _) <- flip runStateT sc $ getGCtx (userRole userInfo) gCtxMap
|
||||
(opTy, fields) <- runReaderT (VQ.validateGQ queryParts) gCtx
|
||||
@ -112,7 +116,7 @@ runHasuraGQ pool isoL userInfo sc queryParts = do
|
||||
"subscriptions are not supported over HTTP, use websockets instead"
|
||||
let tx = R.resolveSelSet userInfo gCtx opTy fields
|
||||
resp <- liftIO (runExceptT $ runTx tx) >>= liftEither
|
||||
return $ encodeGQResp $ GQSuccess resp
|
||||
return $ HResponse (encodeGQResp $ GQSuccess resp) Nothing
|
||||
where
|
||||
gCtxMap = scGCtxMap sc
|
||||
runTx tx = runLazyTx pool isoL $ withUserInfo userInfo tx
|
||||
@ -126,7 +130,7 @@ runRemoteGQ
|
||||
-- ^ the raw request string
|
||||
-> RemoteSchemaInfo
|
||||
-> G.TypedOperationDefinition
|
||||
-> m BL.ByteString
|
||||
-> m HResponse
|
||||
runRemoteGQ manager userInfo reqHdrs q rsi opDef = do
|
||||
let opTy = G._todType opDef
|
||||
when (opTy == G.OperationTypeSubscription) $
|
||||
@ -138,7 +142,9 @@ runRemoteGQ manager userInfo reqHdrs q rsi opDef = do
|
||||
|
||||
res <- liftIO $ try $ Wreq.postWith options (show url) q
|
||||
resp <- either httpThrow return res
|
||||
return $ resp ^. Wreq.responseBody
|
||||
let respHdrs = map (\(k, v) -> Header (bsToTxt $ CI.original k, bsToTxt v)) $
|
||||
filterResponseHeaders $ resp ^. Wreq.responseHeaders
|
||||
return $ HResponse (resp ^. Wreq.responseBody) (Just respHdrs)
|
||||
|
||||
where
|
||||
RemoteSchemaInfo url hdrConf fwdClientHdrs = rsi
|
||||
@ -147,9 +153,4 @@ runRemoteGQ manager userInfo reqHdrs q rsi opDef = do
|
||||
|
||||
userInfoToHdrs = map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) $
|
||||
userInfoToList userInfo
|
||||
filteredHeaders = flip filter reqHdrs $ \(n, _) ->
|
||||
n `notElem` [ "Content-Length", "Content-MD5", "User-Agent", "Host"
|
||||
, "Origin", "Referer" , "Accept", "Accept-Encoding"
|
||||
, "Accept-Language", "Accept-Datetime"
|
||||
, "Cache-Control", "Connection", "DNT"
|
||||
]
|
||||
filteredHeaders = filterRequestHeaders reqHdrs
|
||||
|
@ -24,6 +24,7 @@ import qualified STMContainers.Map as STMMap
|
||||
|
||||
import Control.Concurrent (threadDelay)
|
||||
import qualified Data.IORef as IORef
|
||||
import Hasura.Server.Context
|
||||
|
||||
import Hasura.GraphQL.Resolve (resolveSelSet)
|
||||
import Hasura.GraphQL.Resolve.Context (LazyRespTx)
|
||||
@ -197,7 +198,8 @@ onStart serverEnv wsConn (StartMsg opId q) msgRaw = catchAndIgnore $ do
|
||||
withComplete $ sendConnErr "subscription to remote server is not supported"
|
||||
resp <- runExceptT $ TH.runRemoteGQ httpMgr userInfo reqHdrs
|
||||
msgRaw rsi opDef
|
||||
either postExecErr sendSuccResp resp
|
||||
-- ignore headers when sending response on websocket
|
||||
either postExecErr sendSuccResp (_hrBody <$> resp)
|
||||
sendCompleted
|
||||
|
||||
where
|
||||
|
@ -43,6 +43,7 @@ import Hasura.RQL.DML.QueryTemplate
|
||||
import Hasura.RQL.Types
|
||||
import Hasura.Server.Auth (AuthMode (..),
|
||||
getUserInfo)
|
||||
import Hasura.Server.Context
|
||||
import Hasura.Server.Cors
|
||||
import Hasura.Server.Init
|
||||
import Hasura.Server.Logging
|
||||
@ -146,11 +147,12 @@ logError
|
||||
logError userInfoM req reqBody sc qErr =
|
||||
logResult userInfoM req reqBody sc (Left qErr) Nothing
|
||||
|
||||
|
||||
mkSpockAction
|
||||
:: (MonadIO m)
|
||||
=> (Bool -> QErr -> Value)
|
||||
-> ServerCtx
|
||||
-> Handler BL.ByteString
|
||||
-> Handler HResponse
|
||||
-> ActionT m ()
|
||||
mkSpockAction qErrEncoder serverCtx handler = do
|
||||
req <- request
|
||||
@ -169,7 +171,8 @@ mkSpockAction qErrEncoder serverCtx handler = do
|
||||
t2 <- liftIO getCurrentTime -- for measuring response time purposes
|
||||
|
||||
-- log result
|
||||
logResult (Just userInfo) req reqBody serverCtx result $ Just (t1, t2)
|
||||
logResult (Just userInfo) req reqBody serverCtx (_hrBody <$> result) $
|
||||
Just (t1, t2)
|
||||
either (qErrToResp $ userRole userInfo == adminRole) resToResp result
|
||||
|
||||
where
|
||||
@ -184,8 +187,9 @@ mkSpockAction qErrEncoder serverCtx handler = do
|
||||
logError Nothing req reqBody serverCtx qErr
|
||||
qErrToResp includeInternal qErr
|
||||
|
||||
resToResp resp = do
|
||||
resToResp (HResponse resp mHdrs) = do
|
||||
uncurry setHeader jsonHeader
|
||||
onJust mHdrs $ mapM_ (uncurry setHeader . unHeader)
|
||||
lazyBytes resp
|
||||
|
||||
withLock :: (MonadIO m, MonadError e m)
|
||||
@ -200,11 +204,12 @@ withLock lk action = do
|
||||
acquireLock = liftIO $ takeMVar lk
|
||||
releaseLock = liftIO $ putMVar lk ()
|
||||
|
||||
v1QueryHandler :: RQLQuery -> Handler BL.ByteString
|
||||
v1QueryHandler :: RQLQuery -> Handler HResponse
|
||||
v1QueryHandler query = do
|
||||
lk <- scCacheLock . hcServerCtx <$> ask
|
||||
bool (fst <$> dbAction) (withLock lk dbActionReload) $
|
||||
res <- bool (fst <$> dbAction) (withLock lk dbActionReload) $
|
||||
queryNeedsReload query
|
||||
return $ HResponse res Nothing
|
||||
where
|
||||
-- Hit postgres
|
||||
dbAction = do
|
||||
@ -230,7 +235,7 @@ v1QueryHandler query = do
|
||||
liftIO $ writeIORef scRef newSc'
|
||||
return resp
|
||||
|
||||
v1Alpha1GQHandler :: GH.GraphQLRequest -> Handler BL.ByteString
|
||||
v1Alpha1GQHandler :: GH.GraphQLRequest -> Handler HResponse
|
||||
v1Alpha1GQHandler query = do
|
||||
userInfo <- asks hcUser
|
||||
reqBody <- asks hcReqBody
|
||||
@ -242,14 +247,15 @@ v1Alpha1GQHandler query = do
|
||||
isoL <- scIsolation . hcServerCtx <$> ask
|
||||
GH.runGQ pool isoL userInfo sc manager reqHeaders query reqBody
|
||||
|
||||
gqlExplainHandler :: GE.GQLExplain -> Handler BL.ByteString
|
||||
gqlExplainHandler :: GE.GQLExplain -> Handler HResponse
|
||||
gqlExplainHandler query = do
|
||||
onlyAdmin
|
||||
scRef <- scCacheRef . hcServerCtx <$> ask
|
||||
sc <- liftIO $ readIORef scRef
|
||||
pool <- scPGPool . hcServerCtx <$> ask
|
||||
isoL <- scIsolation . hcServerCtx <$> ask
|
||||
GE.explainGQLQuery pool isoL sc query
|
||||
res <- GE.explainGQLQuery pool isoL sc query
|
||||
return $ HResponse res Nothing
|
||||
|
||||
newtype QueryParser
|
||||
= QueryParser { getQueryParser :: QualifiedTable -> Handler RQLQuery }
|
||||
@ -271,7 +277,7 @@ queryParsers =
|
||||
q <- decodeValue val
|
||||
return $ f q
|
||||
|
||||
legacyQueryHandler :: TableName -> T.Text -> Handler BL.ByteString
|
||||
legacyQueryHandler :: TableName -> T.Text -> Handler HResponse
|
||||
legacyQueryHandler tn queryType =
|
||||
case M.lookup queryType queryParsers of
|
||||
Just queryParser -> getQueryParser queryParser qt >>= v1QueryHandler
|
||||
|
@ -219,12 +219,7 @@ userInfoFromAuthHook logger manager hook reqHeaders = do
|
||||
(Just $ HttpException err) Nothing
|
||||
throw500 "Internal Server Error"
|
||||
|
||||
filteredHeaders = flip filter reqHeaders $ \(n, _) ->
|
||||
n `notElem` [ "Content-Length", "Content-MD5", "User-Agent", "Host"
|
||||
, "Origin", "Referer" , "Accept", "Accept-Encoding"
|
||||
, "Accept-Language", "Accept-Datetime"
|
||||
, "Cache-Control", "Connection", "DNT"
|
||||
]
|
||||
filteredHeaders = filterRequestHeaders reqHeaders
|
||||
|
||||
|
||||
getUserInfo
|
||||
@ -241,7 +236,7 @@ getUserInfo logger manager rawHeaders = \case
|
||||
AMAdminSecret adminScrt unAuthRole ->
|
||||
case adminSecretM of
|
||||
Just givenAdminScrt -> userInfoWhenAdminSecret adminScrt givenAdminScrt
|
||||
Nothing -> userInfoWhenNoAdminSecret unAuthRole
|
||||
Nothing -> userInfoWhenNoAdminSecret unAuthRole
|
||||
|
||||
AMAdminSecretAndHook accKey hook ->
|
||||
whenAdminSecretAbsent accKey (userInfoFromAuthHook logger manager hook rawHeaders)
|
||||
|
22
server/src-lib/Hasura/Server/Context.hs
Normal file
22
server/src-lib/Hasura/Server/Context.hs
Normal file
@ -0,0 +1,22 @@
|
||||
module Hasura.Server.Context
|
||||
( HResponse(..)
|
||||
, Header (..)
|
||||
, Headers
|
||||
)
|
||||
where
|
||||
|
||||
import Hasura.Prelude
|
||||
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
|
||||
newtype Header
|
||||
= Header { unHeader :: (Text, Text) }
|
||||
deriving (Show, Eq)
|
||||
|
||||
type Headers = [Header]
|
||||
|
||||
data HResponse
|
||||
= HResponse
|
||||
{ _hrBody :: !BL.ByteString
|
||||
, _hrHeaders :: !(Maybe Headers)
|
||||
} deriving (Show, Eq)
|
@ -9,11 +9,13 @@ import System.Exit
|
||||
import System.Process
|
||||
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.HashSet as Set
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as TE
|
||||
import qualified Data.Text.Encoding.Error as TE
|
||||
import qualified Data.Text.IO as TI
|
||||
import qualified Language.Haskell.TH.Syntax as TH
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Text.Ginger as TG
|
||||
import qualified Text.Regex.TDFA as TDFA
|
||||
import qualified Text.Regex.TDFA.ByteString as TDFA
|
||||
@ -134,3 +136,31 @@ matchRegex regex caseSensitive src =
|
||||
fmapL :: (a -> a') -> Either a b -> Either a' b
|
||||
fmapL fn (Left e) = Left (fn e)
|
||||
fmapL _ (Right x) = pure x
|
||||
|
||||
|
||||
-- ignore the following request headers from the client
|
||||
filterRequestHeaders :: [HTTP.Header] -> [HTTP.Header]
|
||||
filterRequestHeaders = filterHeaders reqHeaders
|
||||
where
|
||||
reqHeaders = Set.fromList
|
||||
[ "Content-Length", "Content-MD5", "User-Agent", "Host"
|
||||
, "Origin", "Referer" , "Accept", "Accept-Encoding"
|
||||
, "Accept-Language", "Accept-Datetime"
|
||||
, "Cache-Control", "Connection", "DNT"
|
||||
]
|
||||
|
||||
|
||||
-- ignore the following response headers from remote
|
||||
filterResponseHeaders :: [HTTP.Header] -> [HTTP.Header]
|
||||
filterResponseHeaders = filterHeaders respHeaders
|
||||
where
|
||||
respHeaders = Set.fromList
|
||||
[ "Server", "Transfer-Encoding", "Cache-Control"
|
||||
, "Access-Control-Allow-Credentials"
|
||||
, "Access-Control-Allow-Methods"
|
||||
, "Access-Control-Allow-Origin"
|
||||
, "Content-Type"
|
||||
]
|
||||
|
||||
filterHeaders :: Set.HashSet HTTP.HeaderName -> [HTTP.Header] -> [HTTP.Header]
|
||||
filterHeaders list = filter (\(n, _) -> not $ n `Set.member` list)
|
||||
|
@ -169,7 +169,30 @@ class PersonGraphQL(RequestHandler):
|
||||
res = person_schema.execute(req.json['query'])
|
||||
return mkJSONResp(res)
|
||||
|
||||
#GraphQL server with interfaces
|
||||
# GraphQL server that returns Set-Cookie response header
|
||||
class SampleAuth(graphene.ObjectType):
|
||||
hello = graphene.String(arg=graphene.String(default_value="world"))
|
||||
|
||||
def resolve_hello(self, info, arg):
|
||||
return "Hello " + arg
|
||||
|
||||
sample_auth_schema = graphene.Schema(query=SampleAuth,
|
||||
subscription=SampleAuth)
|
||||
|
||||
class SampleAuthGraphQL(RequestHandler):
|
||||
def get(self, request):
|
||||
return Response(HTTPStatus.METHOD_NOT_ALLOWED)
|
||||
|
||||
def post(self, request):
|
||||
if not request.json:
|
||||
return Response(HTTPStatus.BAD_REQUEST)
|
||||
res = hello_schema.execute(request.json['query'])
|
||||
resp = mkJSONResp(res)
|
||||
resp.headers['Set-Cookie'] = 'abcd'
|
||||
return resp
|
||||
|
||||
|
||||
# GraphQL server with interfaces
|
||||
|
||||
class Character(graphene.Interface):
|
||||
id = graphene.ID(required=True)
|
||||
@ -567,15 +590,15 @@ class EchoGraphQL(RequestHandler):
|
||||
if not req.json:
|
||||
return Response(HTTPStatus.BAD_REQUEST)
|
||||
res = echo_schema.execute(req.json['query'])
|
||||
respDict = res.to_dict()
|
||||
typesList = respDict.get('data',{}).get('__schema',{}).get('types',None)
|
||||
resp_dict = res.to_dict()
|
||||
types_list = resp_dict.get('data',{}).get('__schema',{}).get('types', None)
|
||||
#Hack around enum default_value serialization issue: https://github.com/graphql-python/graphql-core/issues/166
|
||||
if typesList is not None:
|
||||
for t in filter(lambda ty: ty['name'] == 'EchoQuery', typesList):
|
||||
if types_list is not None:
|
||||
for t in filter(lambda ty: ty['name'] == 'EchoQuery', types_list):
|
||||
for f in filter(lambda fld: fld['name'] == 'echo', t['fields']):
|
||||
for a in filter(lambda arg: arg['name'] == 'enumInput', f['args']):
|
||||
a['defaultValue'] = 'RED'
|
||||
return Response(HTTPStatus.OK, respDict,
|
||||
return Response(HTTPStatus.OK, resp_dict,
|
||||
{'Content-Type': 'application/json'})
|
||||
|
||||
handlers = MkHandlers({
|
||||
@ -597,7 +620,8 @@ handlers = MkHandlers({
|
||||
'/union-graphql-err-no-member-types' : UnionGraphQLSchemaErrNoMemberTypes,
|
||||
'/union-graphql-err-wrapped-type' : UnionGraphQLSchemaErrWrappedType,
|
||||
'/default-value-echo-graphql' : EchoGraphQL,
|
||||
'/person-graphql': PersonGraphQL
|
||||
'/person-graphql': PersonGraphQL,
|
||||
'/auth-graphql': SampleAuthGraphQL
|
||||
})
|
||||
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
description: Check response headers from remote
|
||||
url: /v1alpha1/graphql
|
||||
status: 200
|
||||
response:
|
||||
data:
|
||||
hello: Hello me
|
||||
response_headers:
|
||||
'Set-Cookie': abcd
|
||||
query:
|
||||
query: |
|
||||
query {
|
||||
hello(arg: "me")
|
||||
}
|
@ -228,6 +228,33 @@ class TestAddRemoteSchemaTbls:
|
||||
assert st_code == 200, resp
|
||||
|
||||
|
||||
class TestRemoteSchemaResponseHeaders():
|
||||
teardown = {"type": "clear_metadata", "args": {}}
|
||||
dir = 'queries/remote_schemas'
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def transact(self, hge_ctx):
|
||||
q = mk_add_remote_q('sample-auth', 'http://localhost:5000/auth-graphql')
|
||||
st_code, resp = hge_ctx.v1q(q)
|
||||
assert st_code == 200, resp
|
||||
yield
|
||||
hge_ctx.v1q(self.teardown)
|
||||
|
||||
def test_response_headers_from_remote(self, hge_ctx):
|
||||
headers = {}
|
||||
if hge_ctx.hge_key:
|
||||
headers = {'x-hasura-admin-secret': hge_ctx.hge_key}
|
||||
q = {'query': 'query { hello (arg: "me") }'}
|
||||
resp = hge_ctx.http.post(hge_ctx.hge_url + '/v1alpha1/graphql', json=q,
|
||||
headers=headers)
|
||||
assert resp.status_code == 200
|
||||
print(resp.headers)
|
||||
assert ('Set-Cookie' in resp.headers and
|
||||
resp.headers['Set-Cookie'] == 'abcd')
|
||||
res = resp.json()
|
||||
assert res['data']['hello'] == "Hello me"
|
||||
|
||||
|
||||
class TestAddRemoteSchemaCompareRootQueryFields:
|
||||
|
||||
remote = 'http://localhost:5000/default-value-echo-graphql'
|
||||
|
Loading…
Reference in New Issue
Block a user