Skip to content

Commit

Permalink
Server: Remote relationships permissions
Browse files Browse the repository at this point in the history
GITHUB_PR_NUMBER: 6125
GITHUB_PR_URL: hasura#6125

Co-authored-by: Karthikeyan Chinnakonda <15602904+codingkarthik@users.noreply.github.com>
GitOrigin-RevId: 53d0671
  • Loading branch information
hasura-bot and codingkarthik committed Jan 19, 2021
1 parent 2c56254 commit 98ccd81
Show file tree
Hide file tree
Showing 34 changed files with 1,029 additions and 209 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ and be accessible according to the permissions that were configured for the role
- server: fix issue with query actions with relationship with permissions configured on the remote table (fix #6385)
- server: always log the `request_id` at the `detail.request_id` path for both `query-log` and `http-log` (#6244)
- server: fix issue with `--stringify-numeric-types` not stringifying aggregate fields (fix #5704)
- server: derive permissions for remote relationship field from the corresponding remote schema's permissions
- console: allow user to cascade Postgres dependencies when dropping Postgres objects (close #5109) (#5248)
- console: mark inconsistent remote schemas in the UI (close #5093) (#5181)
- console: remove ONLY as default for ALTER TABLE in column alter operations (close #5512) #5706
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,94 @@ session variable, then its value is resolved and then added before querying the
In this case, ``"x-hasura-hello"`` will be the argument to the ``hello`` field
whenever it's queried.

Remote Relationship Permissions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Permissions for remote relationships are derived from the role's remote schema permissions.
When permissions for a given remote relationship cannot be derived from the remote schema
permissions of a given role, that remote relationship will not be accessible to that role.

Cases when the remote relationship cannot be derived are:
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""

1. There are no remote schema permissions configured for the role for the remote join's remote schema.
2. The remote join field is not accessible to the role.
3. Any of the type (both output and input types) used in the remote join field is not accessible to the role.

When a remote field's argument contains a preset and the same argument
is used for creating a remote relationship, then the **remote presets will be
overridden by the remote join configuration**. For example:

Let's say we have a table called ``customer`` and we have a remote schema called
``payments`` and we have a remote relationship ``customer_transactions_history`` defined
which joins ``customer`` to ``transactions`` field of the ``payments`` field.

Suppose, the ``payments`` remote schema is defined in the following way:

.. code-block:: graphql
type Transaction {
customer_id Int!
amount Int!
time String!
merchant String!
}
type Query {
transactions(customer_id: String!, limit: Int): [Transaction]
}
And, the ``customer`` table is defined in the following manner.

.. code-block:: sql
CREATE TABLE customer (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
The remote relationship is defined to join the ``id`` field from the
``customer`` table to the ``customer_id`` argument of the ``transactions``
field.

We only allow the ``user`` role to access the ``amount`` and ``time`` fiels of
the ``Transaction`` object, and introduce a preset for the ``limit`` argument
of the ``transaction`` field, resulting in the following schema being presented.

.. code-block:: graphql
type Transaction {
amount Int!
time String!
}
type Query {
transactions(customer_id: String!, limit: Int @preset(value: 10)): [Transaction]
}
Two changes have been made for the ``user`` role:

1. The ``merchant`` and ``customer_id`` fields are not accessible in the ``Transaction`` object.
2. The ``limit`` argument has a preset of 10.

Now, consider the following query:

.. code-block:: graphql
query {
customer {
name
customer_transactions_history {
amount
time
}
}
}
The ``user`` role won't be able to provide the value for the ``limit`` argument in
the ``customer_transactions_history`` field because the ``limit`` has a preset set
and the value will be added by the graphql-engine before it queries the remote schema.

.. _add_remote_schema_permissions_syntax:

Args syntax
Expand Down
65 changes: 36 additions & 29 deletions server/src-lib/Hasura/Backends/Postgres/Execute/RemoteJoin.hs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import Hasura.EncJSON
import Hasura.GraphQL.Parser hiding (field)
import Hasura.GraphQL.RemoteServer (execRemoteGQ)
import Hasura.GraphQL.Transport.HTTP.Protocol
import Hasura.GraphQL.Execute.Remote
import Hasura.RQL.DML.Internal
import Hasura.RQL.IR.RemoteJoin
import Hasura.RQL.IR.Returning
Expand Down Expand Up @@ -305,7 +306,7 @@ data RemoteJoinField
= RemoteJoinField
{ _rjfRemoteSchema :: !RemoteSchemaInfo -- ^ The remote schema server info.
, _rjfAlias :: !Alias -- ^ Top level alias of the field
, _rjfField :: !(G.Field G.NoFragments Variable) -- ^ The field AST
, _rjfField :: !(G.Field G.NoFragments RemoteSchemaVariable) -- ^ The field AST
, _rjfFieldCall :: ![G.Name] -- ^ Path to remote join value
} deriving (Show, Eq)

Expand Down Expand Up @@ -431,22 +432,28 @@ defaultValue = \case
JSONValue _ -> Nothing
GraphQLValue g -> Just g

collectVariables :: G.Value Variable -> HashMap G.VariableDefinition A.Value
collectVariables = \case
collectVariablesFromValue :: G.Value Variable -> HashMap G.VariableDefinition A.Value
collectVariablesFromValue = \case
G.VNull -> mempty
G.VInt _ -> mempty
G.VFloat _ -> mempty
G.VString _ -> mempty
G.VBoolean _ -> mempty
G.VEnum _ -> mempty
G.VList values -> foldl Map.union mempty $ map collectVariables values
G.VObject values -> foldl Map.union mempty $ map collectVariables $ Map.elems values
G.VList values -> foldl Map.union mempty $ map collectVariablesFromValue values
G.VObject values -> foldl Map.union mempty $ map collectVariablesFromValue $ Map.elems values
G.VVariable var@(Variable _ gType val) ->
let name = getName var
jsonVal = inputValueToJSON val
defaultVal = defaultValue val
in Map.singleton (G.VariableDefinition name gType defaultVal) jsonVal

collectVariablesFromField :: G.Field G.NoFragments Variable -> HashMap G.VariableDefinition A.Value
collectVariablesFromField (G.Field _ _ arguments _ selSet) =
let argumentVariables = fmap collectVariablesFromValue arguments
selSetVariables = (fmap snd <$> collectVariablesFromSelectionSet selSet)
in foldl Map.union mempty (Map.elems argumentVariables) <> Map.fromList selSetVariables

-- | Fetch remote join field value from remote servers by batching respective 'RemoteJoinField's
fetchRemoteJoinFields
:: ( HasVersion
Expand All @@ -462,7 +469,8 @@ fetchRemoteJoinFields
-> m AO.Object
fetchRemoteJoinFields env manager reqHdrs userInfo remoteJoins = do
results <- forM (Map.toList remoteSchemaBatch) $ \(rsi, batch) -> do
let gqlReq = fieldsToRequest $ _rjfField <$> batch
resolvedRemoteFields <- traverse (traverse (resolveRemoteVariable userInfo)) $ _rjfField <$> batch
let gqlReq = fieldsToRequest resolvedRemoteFields
-- NOTE: discard remote headers (for now):
(_, _, respBody) <- execRemoteGQ env manager userInfo reqHdrs rsi gqlReq
case AO.eitherDecode respBody of
Expand All @@ -484,7 +492,7 @@ fetchRemoteJoinFields env manager reqHdrs userInfo remoteJoins = do

fieldsToRequest :: NonEmpty (G.Field G.NoFragments Variable) -> GQLReqOutgoing
fieldsToRequest gFields@(headField :| _) =
let variableInfos =
let variableInfos =
-- only the `headField` is used for collecting the variables here because
-- the variable information of all the fields will be the same.
-- For example:
Expand All @@ -497,21 +505,20 @@ fetchRemoteJoinFields env manager reqHdrs userInfo remoteJoins = do
--
-- If there are 10 authors, then there are 10 fields that will be requested
-- each containing exactly the same variable info.
foldMap collectVariables $ G._fArguments headField
in GQLReq
{ _grOperationName = Nothing
, _grVariables =
mapKeys G._vdName variableInfos <$ guard (not $ Map.null variableInfos)
, _grQuery = G.TypedOperationDefinition
{ G._todSelectionSet =
NE.toList $ G.SelectionField . convertFieldWithVariablesToName <$> gFields
, G._todVariableDefinitions = Map.keys variableInfos
, G._todType = G.OperationTypeQuery
, G._todName = Nothing
, G._todDirectives = []
}
}

collectVariablesFromField headField
in GQLReq
{ _grOperationName = Nothing
, _grVariables =
mapKeys G._vdName variableInfos <$ guard (not $ Map.null variableInfos)
, _grQuery = G.TypedOperationDefinition
{ G._todSelectionSet =
NE.toList $ G.SelectionField . convertFieldWithVariablesToName <$> gFields
, G._todVariableDefinitions = Map.keys variableInfos
, G._todType = G.OperationTypeQuery
, G._todName = Nothing
, G._todDirectives = []
}
}

-- | Replace 'RemoteJoinField' in composite JSON with it's json value from remote server response.
replaceRemoteFields
Expand Down Expand Up @@ -546,21 +553,21 @@ replaceRemoteFields compositeJson remoteServerResponse =
-- selection set at the leaf of the tree we construct.
fieldCallsToField
:: forall m. MonadError QErr m
=> Map.HashMap G.Name (InputValue Variable)
=> Map.HashMap G.Name (InputValue RemoteSchemaVariable)
-- ^ user input arguments to the remote join field
-> Map.HashMap G.Name (G.Value Void)
-- ^ Contains the values of the variables that have been defined in the remote join definition
-> G.SelectionSet G.NoFragments Variable
-> G.SelectionSet G.NoFragments RemoteSchemaVariable
-- ^ Inserted at leaf of nested FieldCalls
-> Alias
-- ^ Top-level name to set for this Field
-> NonEmpty FieldCall
-> m (G.Field G.NoFragments Variable)
-> m (G.Field G.NoFragments RemoteSchemaVariable)
fieldCallsToField rrArguments variables finalSelSet topAlias =
fmap (\f -> f{G._fAlias = Just topAlias}) . nest
where
-- almost: `foldr nest finalSelSet`
nest :: NonEmpty FieldCall -> m (G.Field G.NoFragments Variable)
nest :: NonEmpty FieldCall -> m (G.Field G.NoFragments RemoteSchemaVariable)
nest ((FieldCall name remoteArgs) :| rest) = do
templatedArguments <- convert <$> createArguments variables remoteArgs
graphQLarguments <- traverse peel rrArguments
Expand All @@ -577,10 +584,10 @@ fieldCallsToField rrArguments variables finalSelSet topAlias =
in pure (arguments, finalSelSet)
pure $ G.Field Nothing name args [] selSet

convert :: Map.HashMap G.Name (G.Value Void) -> Map.HashMap G.Name (G.Value Variable)
convert :: Map.HashMap G.Name (G.Value Void) -> Map.HashMap G.Name (G.Value RemoteSchemaVariable)
convert = fmap G.literal

peel :: InputValue Variable -> m (G.Value Variable)
peel :: InputValue RemoteSchemaVariable -> m (G.Value RemoteSchemaVariable)
peel = \case
GraphQLValue v -> pure v
JSONValue _ ->
Expand All @@ -596,7 +603,7 @@ fieldCallsToField rrArguments variables finalSelSet topAlias =
-- `where: { id : 1}`
-- And during execution, client also gives the input arg: `where: {name: "tiru"}`
-- We need to merge the input argument to where: {id : 1, name: "tiru"}
mergeValue :: G.Value Variable -> G.Value Variable -> G.Value Variable
mergeValue :: G.Value RemoteSchemaVariable -> G.Value RemoteSchemaVariable -> G.Value RemoteSchemaVariable
mergeValue lVal rVal = case (lVal, rVal) of
(G.VList l, G.VList r) ->
G.VList $ l <> r
Expand Down
7 changes: 7 additions & 0 deletions server/src-lib/Hasura/GraphQL/Execute/Remote.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Hasura.GraphQL.Execute.Remote
( buildExecStepRemote
, collectVariablesFromSelectionSet
, collectVariables
, resolveRemoteVariable
, resolveRemoteField
Expand Down Expand Up @@ -58,6 +59,12 @@ collectVariables
collectVariables =
Set.unions . fmap (foldMap Set.singleton)

collectVariablesFromSelectionSet
:: G.SelectionSet G.NoFragments Variable
-> [(G.VariableDefinition, (G.Name, J.Value))]
collectVariablesFromSelectionSet =
map mkVariableDefinitionAndValue . Set.toList . collectVariables

buildExecStepRemote
:: forall db action
. RemoteSchemaInfo
Expand Down
5 changes: 3 additions & 2 deletions server/src-lib/Hasura/GraphQL/Parser/Class.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import qualified Language.Haskell.TH as TH
import Data.Has
import Data.Text.Extended
import Data.Tuple.Extended
import GHC.Stack (HasCallStack)
import Type.Reflection (Typeable)
import GHC.Stack (HasCallStack)
import Type.Reflection (Typeable)

import Hasura.GraphQL.Parser.Class.Parse
import Hasura.GraphQL.Parser.Internal.Types
Expand All @@ -25,6 +25,7 @@ import Hasura.RQL.Types.Source
import Hasura.RQL.Types.Table
import Hasura.Session (RoleName)


{- Note [Tying the knot]
~~~~~~~~~~~~~~~~~~~~~~~~
GraphQL type definitions can be mutually recursive, and indeed, they quite often
Expand Down
Loading

0 comments on commit 98ccd81

Please sign in to comment.