From 51fc1bfbf311a5cb566ff44b2e9edec315d0d579 Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Sat, 4 May 2019 15:58:27 +0530 Subject: [PATCH 1/8] fix duplicate headers being sent to remote schema following is the order of precedence of headers now: conf headers > resolved userinfo vars > client headers --- server/src-lib/Hasura/GraphQL/Execute.hs | 9 ++++++- server/tests-py/graphql_server.py | 30 ++++++++++++++++++++- server/tests-py/test_schema_stitching.py | 34 ++++++++++++++++++++++-- 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Execute.hs b/server/src-lib/Hasura/GraphQL/Execute.hs index 6a41847de91e0..109fc96fa9d14 100644 --- a/server/src-lib/Hasura/GraphQL/Execute.hs +++ b/server/src-lib/Hasura/GraphQL/Execute.hs @@ -322,7 +322,14 @@ execRemoteGQ manager userInfo reqHdrs q rsi opDef = do hdrs <- getHeadersFromConf hdrConf let confHdrs = map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) hdrs clientHdrs = bool [] filteredHeaders fwdClientHdrs - options = wreqOptions manager (userInfoToHdrs ++ clientHdrs ++ confHdrs) + -- filter out duplicate headers + -- priority: conf headers > resolved userinfo vars > client headers + hdrMaps = [ Map.fromList confHdrs + , Map.fromList userInfoToHdrs + , Map.fromList clientHdrs + ] + finalHdrs = foldr Map.union Map.empty hdrMaps + options = wreqOptions manager (Map.toList finalHdrs) res <- liftIO $ try $ Wreq.postWith options (show url) q resp <- either httpThrow return res diff --git a/server/tests-py/graphql_server.py b/server/tests-py/graphql_server.py index 7f87bec9e7a71..e8db195e08a53 100644 --- a/server/tests-py/graphql_server.py +++ b/server/tests-py/graphql_server.py @@ -578,6 +578,33 @@ def post(self, req): return Response(HTTPStatus.OK, respDict, {'Content-Type': 'application/json'}) + +class HeaderTest(graphene.ObjectType): + hello = graphene.String(arg=graphene.String(default_value="world")) + + def resolve_hello(self, info, arg): + headers = info.context + if not (headers.get_all('x-hasura-test') == ['abcd'] and + headers.get_all('x-hasura-role') == ['user'] and + headers.get_all('x-hasura-user-id') == ['abcd1234'] and + headers.get_all('Authorization') == ['Bearer abcdef']): + raise Exception('headers dont match') + + return "Hello " + arg + +header_test_schema = graphene.Schema(query=HeaderTest) + +class HeaderTestGraphQL(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 = header_test_schema.execute(request.json['query'], + context=request.headers) + return mkJSONResp(res) + handlers = MkHandlers({ '/hello': HelloWorldHandler, '/hello-graphql': HelloGraphQL, @@ -597,7 +624,8 @@ def post(self, req): '/union-graphql-err-no-member-types' : UnionGraphQLSchemaErrNoMemberTypes, '/union-graphql-err-wrapped-type' : UnionGraphQLSchemaErrWrappedType, '/default-value-echo-graphql' : EchoGraphQL, - '/person-graphql': PersonGraphQL + '/person-graphql': PersonGraphQL, + '/header-graphql': HeaderTestGraphQL }) diff --git a/server/tests-py/test_schema_stitching.py b/server/tests-py/test_schema_stitching.py index faf3212ab90f1..53af246fff788 100644 --- a/server/tests-py/test_schema_stitching.py +++ b/server/tests-py/test_schema_stitching.py @@ -12,7 +12,7 @@ from validate import check_query_f, check_query -def mk_add_remote_q(name, url): +def mk_add_remote_q(name, url, headers=None, client_hdrs=False): return { "type": "add_remote_schema", "args": { @@ -20,7 +20,8 @@ def mk_add_remote_q(name, url): "comment": "testing " + name, "definition": { "url": url, - "forward_client_headers": False + "headers": headers, + "forward_client_headers": client_hdrs } } } @@ -236,6 +237,35 @@ def test_add_schema_same_type_containing_same_scalar(self, hge_ctx): hge_ctx.v1q({"type": "remove_remote_schema", "args": {"name": "person-graphql"}}) assert st_code == 200, resp + def test_remote_schema_forward_headers(self, hge_ctx): + """ + test headers from client and conf and resolved info gets passed + correctly to remote schema, and no duplicates are sent. this test just + tests if the remote schema returns success or not. checking of header + duplicate logic is in the remote schema server + """ + conf_hdrs = [{'name': 'x-hasura-test', 'value': 'abcd'}] + add_remote = mk_add_remote_q('header-graphql', + 'http://localhost:5000/header-graphql', + headers=conf_hdrs, client_hdrs=True) + st_code, resp = hge_ctx.v1q(add_remote) + assert st_code == 200, resp + q = {'query': '{ hello }'} + hdrs = { + 'x-hasura-test': 'xyzz', + 'x-hasura-role': 'user', + 'x-hasura-user-id': 'abcd1234', + 'Authorization': 'Bearer abcdef' + } + resp = hge_ctx.post(hge_ctx.hge_url+'/v1alpha1/graphql', json=q, headers=hdrs) + assert resp.status_code == 200 + res = resp.json() + assert 'data' in res + assert res['data']['hello'] == 'Hello world' + + hge_ctx.v1q({"type": "remove_remote_schema", "args": {"name": "header-graphql"}}) + assert st_code == 200, resp + class TestRemoteSchemaQueriesOverWebsocket: dir = 'queries/remote_schemas' From 60350b10b7255968e3198896bdc503342ed53b5f Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Mon, 6 May 2019 11:40:59 +0530 Subject: [PATCH 2/8] fix tests --- server/tests-py/graphql_server.py | 4 ++-- server/tests-py/test_schema_stitching.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/server/tests-py/graphql_server.py b/server/tests-py/graphql_server.py index e8db195e08a53..d4e3f29255aa1 100644 --- a/server/tests-py/graphql_server.py +++ b/server/tests-py/graphql_server.py @@ -580,9 +580,9 @@ def post(self, req): class HeaderTest(graphene.ObjectType): - hello = graphene.String(arg=graphene.String(default_value="world")) + wassup = graphene.String(arg=graphene.String(default_value='world')) - def resolve_hello(self, info, arg): + def resolve_wassup(self, info, arg): headers = info.context if not (headers.get_all('x-hasura-test') == ['abcd'] and headers.get_all('x-hasura-role') == ['user'] and diff --git a/server/tests-py/test_schema_stitching.py b/server/tests-py/test_schema_stitching.py index 53af246fff788..18688a9ee7b38 100644 --- a/server/tests-py/test_schema_stitching.py +++ b/server/tests-py/test_schema_stitching.py @@ -250,20 +250,26 @@ def test_remote_schema_forward_headers(self, hge_ctx): headers=conf_hdrs, client_hdrs=True) st_code, resp = hge_ctx.v1q(add_remote) assert st_code == 200, resp - q = {'query': '{ hello }'} + q = {'query': '{ wassup }'} hdrs = { 'x-hasura-test': 'xyzz', 'x-hasura-role': 'user', 'x-hasura-user-id': 'abcd1234', 'Authorization': 'Bearer abcdef' } - resp = hge_ctx.post(hge_ctx.hge_url+'/v1alpha1/graphql', json=q, headers=hdrs) + if hge_ctx.hge_key: + hdrs['x-hasura-admin-secret'] = hge_ctx.hge_key + + resp = hge_ctx.http.post(hge_ctx.hge_url+'/v1alpha1/graphql', json=q, + headers=hdrs) + print(resp.status_code, resp.json()) assert resp.status_code == 200 res = resp.json() assert 'data' in res - assert res['data']['hello'] == 'Hello world' + assert res['data']['wassup'] == 'Hello world' - hge_ctx.v1q({"type": "remove_remote_schema", "args": {"name": "header-graphql"}}) + hge_ctx.v1q({'type': 'remove_remote_schema', + 'args': {'name': 'header-graphql'}}) assert st_code == 200, resp From 9a1a4e19d97fa4d3a0c448f3629a6fe8267dda16 Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Mon, 6 May 2019 12:50:30 +0530 Subject: [PATCH 3/8] filter uservars from client headers --- server/src-lib/Hasura/GraphQL/Execute.hs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Execute.hs b/server/src-lib/Hasura/GraphQL/Execute.hs index 109fc96fa9d14..b2bc2d0099c00 100644 --- a/server/src-lib/Hasura/GraphQL/Execute.hs +++ b/server/src-lib/Hasura/GraphQL/Execute.hs @@ -40,6 +40,7 @@ import Hasura.HTTP import Hasura.Prelude import Hasura.RQL.DDL.Headers import Hasura.RQL.Types +import Hasura.Server.Utils (bsToTxt) import qualified Hasura.GraphQL.Execute.LiveQuery as EL import qualified Hasura.GraphQL.Execute.Plan as EP @@ -341,10 +342,15 @@ execRemoteGQ manager userInfo reqHdrs q rsi opDef = do httpThrow err = throw500 $ T.pack . show $ err userInfoToHdrs = map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) $ - userInfoToList userInfo - filteredHeaders = flip filter reqHdrs $ \(n, _) -> + userInfoToList userInfo + filteredHeaders = filterUserVars $ 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" ] + + filterUserVars hdrs = + let txHdrs = map (\(n, v) -> (bsToTxt $ CI.original n, bsToTxt v)) hdrs + in map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) $ + filter (\(n, _) -> isUserVar n) txHdrs From 89f914c9c5d848324cf6dbdc4edfa6c8486198c7 Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Mon, 6 May 2019 13:30:39 +0530 Subject: [PATCH 4/8] fix silly mistake --- server/src-lib/Hasura/GraphQL/Execute.hs | 2 +- server/tests-py/graphql_server.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src-lib/Hasura/GraphQL/Execute.hs b/server/src-lib/Hasura/GraphQL/Execute.hs index b2bc2d0099c00..edae31cf35a14 100644 --- a/server/src-lib/Hasura/GraphQL/Execute.hs +++ b/server/src-lib/Hasura/GraphQL/Execute.hs @@ -353,4 +353,4 @@ execRemoteGQ manager userInfo reqHdrs q rsi opDef = do filterUserVars hdrs = let txHdrs = map (\(n, v) -> (bsToTxt $ CI.original n, bsToTxt v)) hdrs in map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) $ - filter (\(n, _) -> isUserVar n) txHdrs + filter (\(n, _) -> not $ isUserVar n) txHdrs diff --git a/server/tests-py/graphql_server.py b/server/tests-py/graphql_server.py index d4e3f29255aa1..d7ceb9f54cc3e 100644 --- a/server/tests-py/graphql_server.py +++ b/server/tests-py/graphql_server.py @@ -584,6 +584,7 @@ class HeaderTest(graphene.ObjectType): def resolve_wassup(self, info, arg): headers = info.context + print('recvd headers: ', headers) if not (headers.get_all('x-hasura-test') == ['abcd'] and headers.get_all('x-hasura-role') == ['user'] and headers.get_all('x-hasura-user-id') == ['abcd1234'] and From b7e919b03f03ea40b4f1d8506fc903f5ca8fa9d2 Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Mon, 6 May 2019 13:39:42 +0530 Subject: [PATCH 5/8] add docs --- docs/graphql/manual/remote-schemas/index.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/graphql/manual/remote-schemas/index.rst b/docs/graphql/manual/remote-schemas/index.rst index 8adfe98f6d4d2..4a91948fe2fc2 100644 --- a/docs/graphql/manual/remote-schemas/index.rst +++ b/docs/graphql/manual/remote-schemas/index.rst @@ -141,6 +141,26 @@ 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 `__. + +Authorization in your remote schema server +------------------------------------------ + +Hasura will forward the resolved ``x-hasura-*`` values as headers to your remote +schema. You can use this information to apply authorization rules in your +server. You don't have to redo authorization in your remote schema server. + +You can also configure Hasura to have: +1. static header values that are sent to the remote server +2. forward all headers from the client (like ``Authorization``, ``Cookie`` headers etc.) + +In case there are multiple headers with same name, the order of precedence is: +configuration headers > resolved user (``x-hasura-*``) variables > client headers + +So for example, if client sends an ``Authorization`` header, and the +configuration also has ``Authorization`` header, the configuration header value +will selected. + + Bypassing Hasura's authorization system for remote schema queries ----------------------------------------------------------------- From f69dec3433b0c2ba594e14d2761e8a33eb484c5f Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Mon, 6 May 2019 13:41:58 +0530 Subject: [PATCH 6/8] minor change --- server/src-lib/Hasura/GraphQL/Execute.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src-lib/Hasura/GraphQL/Execute.hs b/server/src-lib/Hasura/GraphQL/Execute.hs index edae31cf35a14..4310cf104cebd 100644 --- a/server/src-lib/Hasura/GraphQL/Execute.hs +++ b/server/src-lib/Hasura/GraphQL/Execute.hs @@ -353,4 +353,4 @@ execRemoteGQ manager userInfo reqHdrs q rsi opDef = do filterUserVars hdrs = let txHdrs = map (\(n, v) -> (bsToTxt $ CI.original n, bsToTxt v)) hdrs in map (\(k, v) -> (CI.mk $ CS.cs k, CS.cs v)) $ - filter (\(n, _) -> not $ isUserVar n) txHdrs + filter (not . isUserVar . fst) txHdrs From f6ce5e7200a792a799645aae5100e2c2d15b278c Mon Sep 17 00:00:00 2001 From: Anon Ray Date: Mon, 6 May 2019 15:16:15 +0530 Subject: [PATCH 7/8] add x-hasura- caveat in docs --- docs/graphql/manual/remote-schemas/index.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/graphql/manual/remote-schemas/index.rst b/docs/graphql/manual/remote-schemas/index.rst index 4a91948fe2fc2..af69ef42f8019 100644 --- a/docs/graphql/manual/remote-schemas/index.rst +++ b/docs/graphql/manual/remote-schemas/index.rst @@ -150,6 +150,7 @@ schema. You can use this information to apply authorization rules in your server. You don't have to redo authorization in your remote schema server. You can also configure Hasura to have: + 1. static header values that are sent to the remote server 2. forward all headers from the client (like ``Authorization``, ``Cookie`` headers etc.) @@ -160,6 +161,12 @@ So for example, if client sends an ``Authorization`` header, and the configuration also has ``Authorization`` header, the configuration header value will selected. +.. note:: + + The headers from client behave similar to the authorization system. If + ``x-hasura-admin-secret`` is sent, then all ``x-hasura-*`` values from the + client are respected, otherwise they are ignored. + Bypassing Hasura's authorization system for remote schema queries ----------------------------------------------------------------- From 25a118ed122e0b9b67b62aa1f58d9443466ba440 Mon Sep 17 00:00:00 2001 From: rikinsk Date: Mon, 6 May 2019 16:06:04 +0530 Subject: [PATCH 8/8] update docs --- docs/graphql/manual/remote-schemas/index.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/graphql/manual/remote-schemas/index.rst b/docs/graphql/manual/remote-schemas/index.rst index af69ef42f8019..b4cf5eee58bf7 100644 --- a/docs/graphql/manual/remote-schemas/index.rst +++ b/docs/graphql/manual/remote-schemas/index.rst @@ -60,6 +60,8 @@ started is to use one of our boilerplates: - `Boilerplates `__ +.. _merge_remote_schema: + Step 2: Merge remote schema ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -147,9 +149,9 @@ Authorization in your remote schema server Hasura will forward the resolved ``x-hasura-*`` values as headers to your remote schema. You can use this information to apply authorization rules in your -server. You don't have to redo authorization in your remote schema server. +server. You don't have to redo authentication in your remote schema server. -You can also configure Hasura to have: +You can also configure Hasura to have (as shown :ref:`above `): 1. static header values that are sent to the remote server 2. forward all headers from the client (like ``Authorization``, ``Cookie`` headers etc.)