From 9d6182339b8cc50be987c73b7c3f661f336cbbb0 Mon Sep 17 00:00:00 2001 From: Nizar Malangadan Date: Mon, 18 Mar 2019 08:53:04 +0530 Subject: [PATCH 1/8] Support JSON arrays in session variables --- server/src-lib/Hasura/RQL/DDL/Permission.hs | 2 +- .../Hasura/RQL/DDL/Permission/Internal.hs | 42 ++++++++++++++----- .../src-lib/Hasura/RQL/DDL/QueryTemplate.hs | 25 ++++++----- server/src-lib/Hasura/RQL/DML/Count.hs | 2 +- server/src-lib/Hasura/RQL/DML/Delete.hs | 2 +- server/src-lib/Hasura/RQL/DML/Insert.hs | 4 +- server/src-lib/Hasura/RQL/DML/Internal.hs | 18 ++++---- .../src-lib/Hasura/RQL/DML/QueryTemplate.hs | 4 +- server/src-lib/Hasura/RQL/DML/Select.hs | 6 +-- server/src-lib/Hasura/RQL/DML/Update.hs | 9 ++-- server/src-lib/Hasura/RQL/GBoolExp.hs | 22 ++++------ server/src-lib/Hasura/RQL/Types.hs | 17 ++++++++ 12 files changed, 94 insertions(+), 59 deletions(-) diff --git a/server/src-lib/Hasura/RQL/DDL/Permission.hs b/server/src-lib/Hasura/RQL/DDL/Permission.hs index eccb7c7fa5972..65ea402db0ac7 100644 --- a/server/src-lib/Hasura/RQL/DDL/Permission.hs +++ b/server/src-lib/Hasura/RQL/DDL/Permission.hs @@ -111,7 +111,7 @@ procSetObj ti mObj = do fmap HM.fromList $ forM (HM.toList setObj) $ \(pgCol, val) -> do ty <- askPGType fieldInfoMap pgCol $ "column " <> pgCol <<> " not found in table " <>> tn - sqlExp <- valueParser ty val + sqlExp <- (vpParseOne valueParser) ty val return (pgCol, sqlExp) let deps = map (mkColDep "on_type" tn . fst) $ HM.toList setColsSQL return (setColsSQL, depHeaders, deps) diff --git a/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs b/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs index 9e7f01cc92f3f..f8b0a748093fa 100644 --- a/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs +++ b/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs @@ -202,20 +202,40 @@ getDependentHeaders :: BoolExp -> [T.Text] getDependentHeaders (BoolExp boolExp) = flip foldMap boolExp $ \(ColExp _ v) -> getDepHeadersFromVal v -valueParser :: (MonadError QErr m) => PGColType -> Value -> m S.SQLExp -valueParser columnType = \case - -- When it is a special variable - val@(String t) - | isUserVar t -> return $ fromCurSess t - | isReqUserId t -> return $ fromCurSess userIdHeader - | otherwise -> txtRHSBuilder columnType val - -- Typical value as Aeson's value - val -> txtRHSBuilder columnType val +valueParser :: (MonadError QErr m) => ValueParser m S.SQLExp +valueParser = ValueParser parseOne parseMany where + parseOne columnType = \case + -- When it is a special variable + val@(String t) + | isUserVar t -> return $ fromCurSess t columnType + | isReqUserId t -> return $ fromCurSess userIdHeader columnType + | otherwise -> txtRHSBuilder columnType val + -- Typical value as Aeson's value + val -> txtRHSBuilder columnType val + curSess = S.SEUnsafe "current_setting('hasura.user')::json" - fromCurSess hdr = withAnnTy $ withGeoVal columnType $ + fromCurSess hdr colTy = withAnnTy $ withGeoVal colTy $ S.SEOpApp (S.SQLOp "->>") [curSess, S.SELit $ T.toLower hdr] - withAnnTy v = S.SETyAnn v $ S.AnnType $ T.pack $ show columnType + where withAnnTy v = S.SETyAnn v $ S.AnnType $ T.pack $ show colTy + + qualJsonArrElemsTxtF = QualifiedObject (SchemaName "pg_catalog") arrElemsTextF + arrElemsTextF = FunctionName "json_array_elements_text" + selJsonArrElems colTy x = S.SESelect $ S.mkSelect + { S.selExtr = [flip S.Extractor Nothing $ withGeoVal colTy (S.SEIden $ toIden arrElemsTextF) `S.SETyAnn` colTyAnn] + , S.selFrom = Just $ S.FromExp [S.mkFuncFromItem qualJsonArrElemsTxtF [x]] + } + where colTyAnn = S.AnnType $ T.pack $ show colTy + + parseMany columnType v = case v of + (String t) + | isUserVar t -> return [selJsonArrElems columnType $ fromCurSess t PGJSON ] + -- | otherwise -> fmap return $ parseOne columnType v + | otherwise -> throw500 "Unexpected error" + + val -> do + vals <- runAesonParser parseJSON val + indexedForM vals (parseOne columnType) injectDefaults :: QualifiedTable -> QualifiedTable -> Q.Query injectDefaults qv qt = diff --git a/server/src-lib/Hasura/RQL/DDL/QueryTemplate.hs b/server/src-lib/Hasura/RQL/DDL/QueryTemplate.hs index 9f483f9def0e4..26957f2260276 100644 --- a/server/src-lib/Hasura/RQL/DDL/QueryTemplate.hs +++ b/server/src-lib/Hasura/RQL/DDL/QueryTemplate.hs @@ -56,20 +56,19 @@ $(deriveJSON (aesonDrop 3 snakeCase){omitNothingFields=True} ''CreateQueryTempla validateParam :: (QErrM m) - => PGColType - -> Value - -> m PS.SQLExp -validateParam pct val = - case val of - Object _ -> do - tpc <- decodeValue val - withPathK "default" $ - maybe (return ()) validateDefault $ tpcDefault tpc - return $ PS.SELit "NULL" - _ -> txtRHSBuilder pct val + => ValueParser m PS.SQLExp +validateParam = defaultValueParser parseOne where - validateDefault = - void . runAesonParser (convToBin pct) + parseOne pct val = case val of + Object _ -> do + tpc <- decodeValue val + withPathK "default" $ + maybe (return ()) validateDefault $ tpcDefault tpc + return $ PS.SELit "NULL" + _ -> txtRHSBuilder pct val + where + validateDefault = + void . runAesonParser (convToBin pct) mkSelQ :: (QErrM m) => SelectQueryT -> m SelectQuery mkSelQ (DMLQuery tn (SelectG c w o lim offset)) = do diff --git a/server/src-lib/Hasura/RQL/DML/Count.hs b/server/src-lib/Hasura/RQL/DML/Count.hs index 119accc3e169d..30f8a1a2ea26a 100644 --- a/server/src-lib/Hasura/RQL/DML/Count.hs +++ b/server/src-lib/Hasura/RQL/DML/Count.hs @@ -72,7 +72,7 @@ mkSQLCount (CountQueryP1 tn (permFltr, mWc) mDistCols) = -- SELECT count(*) FROM (SELECT * FROM .. WHERE ..) r; validateCountQWith :: (UserInfoM m, QErrM m, CacheRM m) - => (PGColType -> Value -> m S.SQLExp) + => ValueParser m S.SQLExp -> CountQuery -> m CountQueryP1 validateCountQWith prepValBuilder (CountQuery qt mDistCols mWhere) = do diff --git a/server/src-lib/Hasura/RQL/DML/Delete.hs b/server/src-lib/Hasura/RQL/DML/Delete.hs index 4bd9f1645d360..b17f696eebd61 100644 --- a/server/src-lib/Hasura/RQL/DML/Delete.hs +++ b/server/src-lib/Hasura/RQL/DML/Delete.hs @@ -53,7 +53,7 @@ getDeleteDeps (DeleteQueryP1 tn (_, wc) mutFlds uniqCols) = validateDeleteQWith :: (UserInfoM m, QErrM m, CacheRM m) - => (PGColType -> Value -> m S.SQLExp) + => ValueParser m S.SQLExp -> DeleteQuery -> m DeleteQueryP1 validateDeleteQWith prepValBuilder (DeleteQuery tableName rqlBE mRetCols) = do diff --git a/server/src-lib/Hasura/RQL/DML/Insert.hs b/server/src-lib/Hasura/RQL/DML/Insert.hs index 58610536bd9ef..d10254a896eab 100644 --- a/server/src-lib/Hasura/RQL/DML/Insert.hs +++ b/server/src-lib/Hasura/RQL/DML/Insert.hs @@ -167,7 +167,7 @@ buildConflictClause tableInfo inpCols (OnConflict mTCol mTCons act) = convInsertQuery :: (UserInfoM m, QErrM m, CacheRM m) => (Value -> m [InsObj]) - -> (PGColType -> Value -> m S.SQLExp) + -> ValueParser m S.SQLExp -> InsertQuery -> m InsertQueryP1 convInsertQuery objsParser prepFn (InsertQuery tableName val oC mRetCols) = do @@ -207,7 +207,7 @@ convInsertQuery objsParser prepFn (InsertQuery tableName val oC mRetCols) = do insView = ipiView insPerm insTuples <- withPathK "objects" $ indexedForM insObjs $ \obj -> - convObj prepFn defInsVals setInsVals fieldInfoMap obj + convObj (vpParseOne prepFn) defInsVals setInsVals fieldInfoMap obj let sqlExps = map snd insTuples inpCols = HS.toList $ HS.fromList $ concatMap fst insTuples diff --git a/server/src-lib/Hasura/RQL/DML/Internal.hs b/server/src-lib/Hasura/RQL/DML/Internal.hs index d83f5280c393b..f9da45f34a90f 100644 --- a/server/src-lib/Hasura/RQL/DML/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Internal.hs @@ -144,13 +144,15 @@ checkPermOnCol pt allowedCols pgCol = do , permTypeToCode pt <> " column " <>> pgCol ] -binRHSBuilder - :: PGColType -> Value -> DMLP1 S.SQLExp -binRHSBuilder colType val = do - preparedArgs <- get - binVal <- runAesonParser (convToBin colType) val - put (preparedArgs DS.|> binVal) - return $ toPrepParam (DS.length preparedArgs + 1) colType +binRHSBuilder :: ValueParser DMLP1 S.SQLExp +binRHSBuilder = defaultValueParser parseOne + where + parseOne colType val = do + preparedArgs <- get + binVal <- runAesonParser (convToBin colType) val + put (preparedArgs DS.|> binVal) + return $ toPrepParam (DS.length preparedArgs + 1) colType + fetchRelTabInfo :: (QErrM m, CacheRM m) @@ -210,7 +212,7 @@ convBoolExp' => FieldInfoMap -> SelPermInfo -> BoolExp - -> (PGColType -> Value -> m S.SQLExp) + -> ValueParser m S.SQLExp -> m AnnBoolExpSQL convBoolExp' cim spi be prepValBuilder = do abe <- annBoolExp prepValBuilder cim be diff --git a/server/src-lib/Hasura/RQL/DML/QueryTemplate.hs b/server/src-lib/Hasura/RQL/DML/QueryTemplate.hs index 3ba5667159bf9..0080620a88eaf 100644 --- a/server/src-lib/Hasura/RQL/DML/QueryTemplate.hs +++ b/server/src-lib/Hasura/RQL/DML/QueryTemplate.hs @@ -70,7 +70,7 @@ buildPrepArg args pct val = Object _ -> do tpc <- decodeValue val v <- getParamValue args tpc - modifyErr (withParamErrMsg tpc) $ binRHSBuilder pct v + modifyErr (withParamErrMsg tpc) $ (vpParseOne binRHSBuilder) pct v _ -> txtRHSBuilder pct val where withParamErrMsg tpc t = @@ -111,7 +111,7 @@ convQT args qt = case qt of v <- getParamValue args tpc R.decodeInsObjs v - f = buildPrepArg args + f = defaultValueParser $ buildPrepArg args execQueryTemplateP1 :: (UserInfoM m, QErrM m, CacheRM m, HasSQLGenCtx m) diff --git a/server/src-lib/Hasura/RQL/DML/Select.hs b/server/src-lib/Hasura/RQL/DML/Select.hs index e41ec2ea32422..238c664266292 100644 --- a/server/src-lib/Hasura/RQL/DML/Select.hs +++ b/server/src-lib/Hasura/RQL/DML/Select.hs @@ -144,7 +144,7 @@ convSelectQ => FieldInfoMap -- Table information of current table -> SelPermInfo -- Additional select permission info -> SelectQExt -- Given Select Query - -> (PGColType -> Value -> m S.SQLExp) + -> ValueParser m S.SQLExp -> m AnnSel convSelectQ fieldInfoMap selPermInfo selQ prepValBuilder = do @@ -207,7 +207,7 @@ convExtRel -> RelName -> Maybe RelName -> SelectQExt - -> (PGColType -> Value -> m S.SQLExp) + -> ValueParser m S.SQLExp -> m (Either ObjSel ArrSel) convExtRel fieldInfoMap relName mAlias selQ prepValBuilder = do -- Point to the name key @@ -279,7 +279,7 @@ getSelectDeps (AnnSelG flds tabFrm _ tableArgs _) = convSelectQuery :: (UserInfoM m, QErrM m, CacheRM m, HasSQLGenCtx m) - => (PGColType -> Value -> m S.SQLExp) + => ValueParser m S.SQLExp -> SelectQuery -> m AnnSel convSelectQuery prepArgBuilder (DMLQuery qt selQ) = do diff --git a/server/src-lib/Hasura/RQL/DML/Update.hs b/server/src-lib/Hasura/RQL/DML/Update.hs index 63a4fc77f2c5a..4771e0c9cfce6 100644 --- a/server/src-lib/Hasura/RQL/DML/Update.hs +++ b/server/src-lib/Hasura/RQL/DML/Update.hs @@ -120,7 +120,7 @@ convOp fieldInfoMap preSetCols updPerm objs conv = validateUpdateQueryWith :: (UserInfoM m, QErrM m, CacheRM m) - => (PGColType -> Value -> m S.SQLExp) + => ValueParser m S.SQLExp -> UpdateQuery -> m UpdateQueryP1 validateUpdateQueryWith f uq = do @@ -147,15 +147,16 @@ validateUpdateQueryWith f uq = do uniqCols = getUniqCols (getCols fieldInfoMap) $ tiUniqOrPrimConstraints tableInfo + let f' = vpParseOne f -- convert the object to SQL set expression setItems <- withPathK "$set" $ - convOp fieldInfoMap preSetCols updPerm (M.toList $ uqSet uq) $ convSet f + convOp fieldInfoMap preSetCols updPerm (M.toList $ uqSet uq) $ convSet f' incItems <- withPathK "$inc" $ - convOp fieldInfoMap preSetCols updPerm (M.toList $ uqInc uq) $ convInc f + convOp fieldInfoMap preSetCols updPerm (M.toList $ uqInc uq) $ convInc f' mulItems <- withPathK "$mul" $ - convOp fieldInfoMap preSetCols updPerm (M.toList $ uqMul uq) $ convMul f + convOp fieldInfoMap preSetCols updPerm (M.toList $ uqMul uq) $ convMul f' defItems <- withPathK "$default" $ convOp fieldInfoMap preSetCols updPerm (zip (uqDefault uq) [()..]) convDefault diff --git a/server/src-lib/Hasura/RQL/GBoolExp.hs b/server/src-lib/Hasura/RQL/GBoolExp.hs index cb1e3d2e192ae..adcb0e9ff3797 100644 --- a/server/src-lib/Hasura/RQL/GBoolExp.hs +++ b/server/src-lib/Hasura/RQL/GBoolExp.hs @@ -24,7 +24,7 @@ parseOpExp -> FieldInfoMap -> PGColInfo -> (T.Text, Value) -> m (OpExpG a) -parseOpExp parser fim (PGColInfo cn colTy _) (opStr, val) = withErrPath $ +parseOpExp (ValueParser parseOneFn parseManyFn) fim (PGColInfo cn colTy _) (opStr, val) = withErrPath $ case opStr of "$eq" -> parseEq "_eq" -> parseEq @@ -163,8 +163,8 @@ parseOpExp parser fim (PGColInfo cn colTy _) (opStr, val) = withErrPath $ parseSTDWithinObj = do WithinOp distVal fromVal <- parseVal - dist <- withPathK "distance" $ parser PGFloat distVal - from <- withPathK "from" $ parser colTy fromVal + dist <- withPathK "distance" $ parseOneFn PGFloat distVal + from <- withPathK "from" $ parseOneFn colTy fromVal return $ ASTDWithin $ WithinOp dist from decodeAndValidateRhsCol = @@ -182,11 +182,9 @@ parseOpExp parser fim (PGColInfo cn colTy _) (opStr, val) = withErrPath $ geometryOnlyOp ty = throwError $ buildMsg ty [PGGeometry] - parseWithTy ty = parser ty val - parseOne = parseWithTy colTy - parseMany = do - vals <- runAesonParser parseJSON val - indexedForM vals (parser colTy) + parseWithTy ty = parseOneFn ty val + parseOne = parseOneFn colTy val + parseMany = parseManyFn colTy val parseVal :: (FromJSON a, QErrM m) => m a parseVal = decodeValue val @@ -198,11 +196,9 @@ parseOpExps -> PGColInfo -> Value -> m [OpExpG a] -parseOpExps valParser cim colInfo = \case - (Object o) -> mapM (parseOpExp valParser cim colInfo)(M.toList o) - val -> pure . AEQ False <$> valParser (pgiType colInfo) val - -type ValueParser m a = PGColType -> Value -> m a +parseOpExps vp@(ValueParser parseOne _)cim colInfo = \case + (Object o) -> mapM (parseOpExp vp cim colInfo)(M.toList o) + val -> pure . AEQ False <$> parseOne (pgiType colInfo) val buildMsg :: PGColType -> [PGColType] -> QErr buildMsg ty expTys = diff --git a/server/src-lib/Hasura/RQL/Types.hs b/server/src-lib/Hasura/RQL/Types.hs index a8d488363b998..0f9e8182d8edd 100644 --- a/server/src-lib/Hasura/RQL/Types.hs +++ b/server/src-lib/Hasura/RQL/Types.hs @@ -42,6 +42,8 @@ module Hasura.RQL.Types , HeaderObj , liftMaybe + , ValueParser(..) + , defaultValueParser , module R ) where @@ -359,3 +361,18 @@ successMsg :: BL.ByteString successMsg = "{\"message\":\"success\"}" type HeaderObj = M.HashMap T.Text T.Text + +--type ValueParser m a = PGColType -> Value -> m a +data ValueParser m a + = ValueParser + { vpParseOne :: PGColType -> Value -> m a + , vpParseMany :: PGColType -> Value -> m [a] + } + +defaultValueParser + :: MonadError QErr m + => (PGColType -> Value -> m a) -> ValueParser m a +defaultValueParser parseOne = ValueParser parseOne parseMany + where parseMany colTy val = do + vals <- runAesonParser parseJSON val + indexedForM vals (parseOne colTy) From 554e6476a0ab381cf50dab891d5777f5be7c8a87 Mon Sep 17 00:00:00 2001 From: Nizar Malangadan Date: Tue, 19 Mar 2019 21:43:20 +0530 Subject: [PATCH 2/8] Tests for JSON arrays in session variables --- server/src-lib/Hasura/RQL/DML/Count.hs | 1 - server/src-lib/Hasura/RQL/DML/Delete.hs | 1 - server/src-lib/Hasura/RQL/DML/Internal.hs | 1 - server/src-lib/Hasura/RQL/DML/Select.hs | 1 - .../agent_delete_perm_arr_sess_var.yaml | 28 ++++++++++++ .../agent_delete_perm_arr_sess_var_fail.yaml | 19 ++++++++ .../delete/permissions/setup.yaml | 24 +++++++++- .../delete/permissions/teardown.yaml | 2 +- ...e_arr_sess_var_editor_allowed_user_id.yaml | 33 ++++++++++++++ ...s_var_editors_err_not_allowed_user_id.yaml | 31 +++++++++++++ .../insert/permissions/setup.yaml | 24 ++++++++++ .../graphql_query/permissions/setup.yaml | 14 ++++++ ..._jsonb_values_filter_arr_session_vars.yaml | 44 +++++++++++++++++++ server/tests-py/test_graphql_mutations.py | 12 +++++ server/tests-py/test_graphql_queries.py | 3 ++ 15 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 server/tests-py/queries/graphql_mutation/delete/permissions/agent_delete_perm_arr_sess_var.yaml create mode 100644 server/tests-py/queries/graphql_mutation/delete/permissions/agent_delete_perm_arr_sess_var_fail.yaml create mode 100644 server/tests-py/queries/graphql_mutation/insert/permissions/insert_article_arr_sess_var_editor_allowed_user_id.yaml create mode 100644 server/tests-py/queries/graphql_mutation/insert/permissions/insert_article_arr_sess_var_editors_err_not_allowed_user_id.yaml create mode 100644 server/tests-py/queries/graphql_query/permissions/user_can_query_jsonb_values_filter_arr_session_vars.yaml diff --git a/server/src-lib/Hasura/RQL/DML/Count.hs b/server/src-lib/Hasura/RQL/DML/Count.hs index 30f8a1a2ea26a..354ff9c7cfdcf 100644 --- a/server/src-lib/Hasura/RQL/DML/Count.hs +++ b/server/src-lib/Hasura/RQL/DML/Count.hs @@ -7,7 +7,6 @@ module Hasura.RQL.DML.Count , countQToTx ) where -import Data.Aeson import Instances.TH.Lift () import qualified Data.ByteString.Builder as BB diff --git a/server/src-lib/Hasura/RQL/DML/Delete.hs b/server/src-lib/Hasura/RQL/DML/Delete.hs index b17f696eebd61..66e008a4efb81 100644 --- a/server/src-lib/Hasura/RQL/DML/Delete.hs +++ b/server/src-lib/Hasura/RQL/DML/Delete.hs @@ -7,7 +7,6 @@ module Hasura.RQL.DML.Delete , runDelete ) where -import Data.Aeson import Instances.TH.Lift () import qualified Data.Sequence as DS diff --git a/server/src-lib/Hasura/RQL/DML/Internal.hs b/server/src-lib/Hasura/RQL/DML/Internal.hs index f9da45f34a90f..a4f32805fce43 100644 --- a/server/src-lib/Hasura/RQL/DML/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Internal.hs @@ -11,7 +11,6 @@ import Hasura.SQL.Types import Hasura.SQL.Value import Control.Lens -import Data.Aeson.Types import qualified Data.HashMap.Strict as M import qualified Data.HashSet as HS diff --git a/server/src-lib/Hasura/RQL/DML/Select.hs b/server/src-lib/Hasura/RQL/DML/Select.hs index 238c664266292..f6f0be36a9ca3 100644 --- a/server/src-lib/Hasura/RQL/DML/Select.hs +++ b/server/src-lib/Hasura/RQL/DML/Select.hs @@ -9,7 +9,6 @@ module Hasura.RQL.DML.Select ) where -import Data.Aeson.Types import Instances.TH.Lift () import qualified Data.HashMap.Strict as HM diff --git a/server/tests-py/queries/graphql_mutation/delete/permissions/agent_delete_perm_arr_sess_var.yaml b/server/tests-py/queries/graphql_mutation/delete/permissions/agent_delete_perm_arr_sess_var.yaml new file mode 100644 index 0000000000000..e8c8430b5d71e --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/delete/permissions/agent_delete_perm_arr_sess_var.yaml @@ -0,0 +1,28 @@ +description: Delete a row in resident table where id=1 and in allowed-resident-ids +url: /v1alpha1/graphql +status: 200 +response: + data: + delete_resident: + affected_rows: 1 + returning: + - age: 25 + id: 1 + name: Griffin +headers: + X-Hasura-Role: agent + X-Hasura-Allowed-Resident-Ids: '[1,2]' +query: + query: | + mutation{ + delete_resident( + where: {id: {_eq: 1}} + ){ + affected_rows + returning { + id + name + age + } + } + } diff --git a/server/tests-py/queries/graphql_mutation/delete/permissions/agent_delete_perm_arr_sess_var_fail.yaml b/server/tests-py/queries/graphql_mutation/delete/permissions/agent_delete_perm_arr_sess_var_fail.yaml new file mode 100644 index 0000000000000..663e5c78d4312 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/delete/permissions/agent_delete_perm_arr_sess_var_fail.yaml @@ -0,0 +1,19 @@ +description: Delete a row in resident table where id=1 and in allowed-resident-ids +url: /v1alpha1/graphql +status: 200 +response: + data: + delete_resident: + affected_rows: 0 +headers: + X-Hasura-Role: agent + X-Hasura-Allowed-Resident-Ids: '[1,3,5]' +query: + query: | + mutation{ + delete_resident( + where: {id: {_eq: 2}} + ){ + affected_rows + } + } diff --git a/server/tests-py/queries/graphql_mutation/delete/permissions/setup.yaml b/server/tests-py/queries/graphql_mutation/delete/permissions/setup.yaml index c2bf87618e8ad..45995cbe9f171 100644 --- a/server/tests-py/queries/graphql_mutation/delete/permissions/setup.yaml +++ b/server/tests-py/queries/graphql_mutation/delete/permissions/setup.yaml @@ -6,7 +6,7 @@ args: args: sql: | create table author( - id serial primary key, + id serial primary key, name text unique, payments_done boolean not null default false ); @@ -148,3 +148,25 @@ args: permission: filter: id: X-Hasura-Resident-Id + +- type: create_delete_permission + args: + table: resident + role: agent + permission: + filter: + id: + $in: X-Hasura-Allowed-Resident-Ids + +- type: create_select_permission + args: + table: resident + role: agent + permission: + columns: + - id + - name + - age + filter: + id: + $in: X-Hasura-Allowed-Resident-Ids diff --git a/server/tests-py/queries/graphql_mutation/delete/permissions/teardown.yaml b/server/tests-py/queries/graphql_mutation/delete/permissions/teardown.yaml index c81c7e5344c7f..c0f6e50db81f3 100644 --- a/server/tests-py/queries/graphql_mutation/delete/permissions/teardown.yaml +++ b/server/tests-py/queries/graphql_mutation/delete/permissions/teardown.yaml @@ -7,7 +7,7 @@ args: table: schema: public name: author - + - type: run_sql args: sql: | diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/insert_article_arr_sess_var_editor_allowed_user_id.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/insert_article_arr_sess_var_editor_allowed_user_id.yaml new file mode 100644 index 0000000000000..d4afa5fb8596d --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/insert_article_arr_sess_var_editor_allowed_user_id.yaml @@ -0,0 +1,33 @@ +#Inserting article data +description: Editor insert article for an allowed user-id +url: /v1alpha1/graphql +status: 200 +headers: + X-Hasura-Role: editor + X-Hasura-Allowed-User-Ids: '[2,3]' +response: + data: + insert_article: + returning: + - content: Sample article content 4 + id: 4 + title: Article 4 +query: + query: | + mutation insert_article { + insert_article ( + objects: [ + { + title: "Article 4", + content: "Sample article content 4", + author_id: 2 + }, + ] + ) { + returning { + id + title + content + } + } + } diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/insert_article_arr_sess_var_editors_err_not_allowed_user_id.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/insert_article_arr_sess_var_editors_err_not_allowed_user_id.yaml new file mode 100644 index 0000000000000..9b49905223ef9 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/insert_article_arr_sess_var_editors_err_not_allowed_user_id.yaml @@ -0,0 +1,31 @@ +description: Editor inserts article data for a not allowed user-id +url: /v1alpha1/graphql +status: 400 +headers: + X-Hasura-Role: editor + X-Hasura-Allowed-User-Ids: '[1,3,4]' +response: + errors: + - extensions: + path: $.selectionSet.insert_article.args.objects + code: permission-error + message: Check constraint violation. insert check constraint failed +query: + query: | + mutation insert_article { + insert_article ( + objects: [ + { + title: "Article 4", + content: "Sample article content 4", + author_id: 2 + }, + ] + ) { + returning { + id + title + content + } + } + } diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/setup.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/setup.yaml index 8d30e66662df7..0425c7349b151 100644 --- a/server/tests-py/queries/graphql_mutation/insert/permissions/setup.yaml +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/setup.yaml @@ -107,6 +107,19 @@ args: - author_id: X-HASURA-USER-ID - is_published: true +#Article select permission for editor +- type: create_select_permission + args: + table: article + role: editor + permission: + columns: '*' + filter: + $or: + - author_id: + $in: X-Hasura-Allowed-User-Ids + - is_published: true + #Article insert permission for user - type: create_insert_permission args: @@ -116,6 +129,17 @@ args: check: author_id: X-Hasura-User-Id +#Article insert permission for editor +#Editor can create articles for some of the users +- type: create_insert_permission + args: + table: article + role: editor + permission: + check: + author_id: + $in: X-Hasura-Allowed-User-Ids + #Article udpate permission for user - type: create_update_permission args: diff --git a/server/tests-py/queries/graphql_query/permissions/setup.yaml b/server/tests-py/queries/graphql_query/permissions/setup.yaml index 06408f4677cea..acaa2904767ef 100644 --- a/server/tests-py/queries/graphql_query/permissions/setup.yaml +++ b/server/tests-py/queries/graphql_query/permissions/setup.yaml @@ -78,6 +78,7 @@ args: - title - content - is_published + - author_id filter: is_published: true @@ -380,6 +381,19 @@ args: jsonb_col: $has_key: X-Hasura-Has-Key +# Using jsonb column and $nin operator +- type: create_select_permission + args: + table: jsonb_table + role: user3 + permission: + columns: + - id + - jsonb_col + filter: + jsonb_col: + $nin: X-Hasura-Protected-Jsons + #Insert data - type: insert args: diff --git a/server/tests-py/queries/graphql_query/permissions/user_can_query_jsonb_values_filter_arr_session_vars.yaml b/server/tests-py/queries/graphql_query/permissions/user_can_query_jsonb_values_filter_arr_session_vars.yaml new file mode 100644 index 0000000000000..33a7657cd3b23 --- /dev/null +++ b/server/tests-py/queries/graphql_query/permissions/user_can_query_jsonb_values_filter_arr_session_vars.yaml @@ -0,0 +1,44 @@ +- description: User can query jsonb values which satisfies filter in select permission with array session variable + url: /v1alpha1/graphql + status: 200 + headers: + X-Hasura-Role: user3 + X-Hasura-Protected-Jsons: |- + [ { "name": "Hasura", "age": 7} ] + response: + data: + jsonb_table: + - id: 2 + jsonb_col: + name: Cross + query: + query: | + query { + jsonb_table{ + id + jsonb_col + } + } + +- description: User can query jsonb values which satisfies filter in select permission with array session variable + url: /v1alpha1/graphql + status: 200 + headers: + X-Hasura-Role: user3 + X-Hasura-Protected-Jsons: |- + [ { "name": "Cross"} ] + response: + data: + jsonb_table: + - id: 1 + jsonb_col: + name: Hasura + age: 7 + query: + query: | + query { + jsonb_table{ + id + jsonb_col + } + } diff --git a/server/tests-py/test_graphql_mutations.py b/server/tests-py/test_graphql_mutations.py index 9fd55f7a547d2..56fe7cb108fd3 100644 --- a/server/tests-py/test_graphql_mutations.py +++ b/server/tests-py/test_graphql_mutations.py @@ -131,6 +131,12 @@ def test_resident_5_modifies_resident_6_upsert(self, hge_ctx): def test_blog_on_conflict_update_preset(self, hge_ctx): check_query_f(hge_ctx, self.dir() + "/blog_on_conflict_update_preset.yaml") + def test_arr_sess_var_insert_article_as_editor_allowed_user_id(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/insert_article_arr_sess_var_editor_allowed_user_id.yaml") + + def test_arr_sess_var_insert_article_as_editor_err_not_allowed_user_id(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/insert_article_arr_sess_var_editors_err_not_allowed_user_id.yaml") + @classmethod def dir(cls): return "queries/graphql_mutation/insert/permissions" @@ -368,6 +374,12 @@ def test_resident_delete_without_select_perm_fail(self, hge_ctx): check_query_f(hge_ctx, self.dir() + "/resident_delete_without_select_perm_fail.yaml") hge_ctx.may_skip_test_teardown = True + def test_agent_delete_perm_arr_sess_var(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/agent_delete_perm_arr_sess_var.yaml") + + def test_agent_delete_perm_arr_sess_var_fail(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/agent_delete_perm_arr_sess_var_fail.yaml") + @classmethod def dir(cls): return "queries/graphql_mutation/delete/permissions" diff --git a/server/tests-py/test_graphql_queries.py b/server/tests-py/test_graphql_queries.py index 64f7691c989f8..58217c223fb9c 100644 --- a/server/tests-py/test_graphql_queries.py +++ b/server/tests-py/test_graphql_queries.py @@ -212,6 +212,9 @@ def test_user_can_query_jsonb_values_filter(self, hge_ctx): def test_user_can_query_jsonb_values_filter_session_vars(self, hge_ctx): check_query_f(hge_ctx, self.dir() + '/user_can_query_jsonb_values_filter_session_vars.yaml') + def test_user_can_query_jsonb_values_filter_arr_session_vars(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + '/user_can_query_jsonb_values_filter_arr_session_vars.yaml') + def test_artist_select_query_Track_fail(self, hge_ctx): check_query_f(hge_ctx, self.dir() + '/artist_select_query_Track_fail.yaml') From 00864a5ebdb349419cc06fd072e159bf0ad540aa Mon Sep 17 00:00:00 2001 From: Nizar Malangadan Date: Tue, 19 Mar 2019 22:06:18 +0530 Subject: [PATCH 3/8] Unexpected payload error message when the input to parseMany is a string, but not a session variable --- server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs b/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs index f8b0a748093fa..30386018f4f9c 100644 --- a/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs +++ b/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs @@ -230,8 +230,7 @@ valueParser = ValueParser parseOne parseMany parseMany columnType v = case v of (String t) | isUserVar t -> return [selJsonArrElems columnType $ fromCurSess t PGJSON ] - -- | otherwise -> fmap return $ parseOne columnType v - | otherwise -> throw500 "Unexpected error" + | otherwise -> throw400 UnexpectedPayload "Expected Array, encountered String" val -> do vals <- runAesonParser parseJSON val From 20202f56c068bab0c85b0797cfc1ce6c70f192b4 Mon Sep 17 00:00:00 2001 From: rikinsk Date: Tue, 26 Mar 2019 13:43:14 +0530 Subject: [PATCH 4/8] handle string value for array operators in console permission builder --- .../PermissionBuilder/PermissionBuilder.js | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/console/src/components/Services/Data/TablePermissions/PermissionBuilder/PermissionBuilder.js b/console/src/components/Services/Data/TablePermissions/PermissionBuilder/PermissionBuilder.js index d0e5f534a0485..bd2009e112d83 100644 --- a/console/src/components/Services/Data/TablePermissions/PermissionBuilder/PermissionBuilder.js +++ b/console/src/components/Services/Data/TablePermissions/PermissionBuilder/PermissionBuilder.js @@ -410,6 +410,14 @@ class PermissionBuilder extends React.Component { /********************************/ + const sessionVariableSuggestion = dispatchInput => { + return renderSuggestion(dispatchInput, 'X-Hasura-User-Id'); + }; + + const jsonSuggestion = dispatchInput => { + return renderSuggestion(dispatchInput, '{}', 'JSON'); + }; + const renderValue = (dispatchFunc, value, prefix, valueType) => { const dispatchInput = val => { let _val = val; @@ -440,14 +448,6 @@ class PermissionBuilder extends React.Component { return renderInput(dispatchInput, value); }; - const sessionVariableSuggestion = () => { - return renderSuggestion(dispatchInput, 'X-Hasura-User-Id'); - }; - - const jsonSuggestion = () => { - return renderSuggestion(dispatchInput, '{}', 'JSON'); - }; - let input; let suggestion; @@ -459,10 +459,10 @@ class PermissionBuilder extends React.Component { PGTypes.geography.includes(valueType) ) { input = inputBox(); - suggestion = jsonSuggestion(); + suggestion = jsonSuggestion(dispatchInput); } else { input = wrapDoubleQuotes(inputBox()); - suggestion = sessionVariableSuggestion(); + suggestion = sessionVariableSuggestion(dispatchInput); } return ( @@ -473,7 +473,12 @@ class PermissionBuilder extends React.Component { }; const renderValueArray = (dispatchFunc, values, prefix, valueType) => { - const _inputArray = []; + const dispatchInput = val => { + dispatchFunc({ prefix: prefix, value: val }); + }; + + const inputArray = []; + (values || []).concat(['']).map((val, i) => { const input = renderValue( dispatchFunc, @@ -481,17 +486,25 @@ class PermissionBuilder extends React.Component { addToPrefix(prefix, i), valueType ); - _inputArray.push(input); + inputArray.push(input); }); const unselectedElements = [(values || []).length]; - return ( + const _inputArray = ( ); + + const _suggestion = sessionVariableSuggestion(dispatchInput); + + return ( + + {_inputArray} {_suggestion} + + ); }; const renderOperatorExp = (dispatchFunc, expression, prefix, valueType) => { @@ -529,7 +542,10 @@ class PermissionBuilder extends React.Component { let _valueInput = ''; if (operator) { - if (isArrayTypeColumnOperator(operator)) { + if ( + isArrayTypeColumnOperator(operator) && + operationValue instanceof Array + ) { _valueInput = renderValueArray( dispatchFunc, operationValue, From 30d2a877d69e4b9e68b710a0a328cc5df74a875c Mon Sep 17 00:00:00 2001 From: rikinsk Date: Tue, 26 Mar 2019 15:12:54 +0530 Subject: [PATCH 5/8] change array session var suggestion --- .../PermissionBuilder/PermissionBuilder.js | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/console/src/components/Services/Data/TablePermissions/PermissionBuilder/PermissionBuilder.js b/console/src/components/Services/Data/TablePermissions/PermissionBuilder/PermissionBuilder.js index bd2009e112d83..269afd39ff640 100644 --- a/console/src/components/Services/Data/TablePermissions/PermissionBuilder/PermissionBuilder.js +++ b/console/src/components/Services/Data/TablePermissions/PermissionBuilder/PermissionBuilder.js @@ -410,14 +410,6 @@ class PermissionBuilder extends React.Component { /********************************/ - const sessionVariableSuggestion = dispatchInput => { - return renderSuggestion(dispatchInput, 'X-Hasura-User-Id'); - }; - - const jsonSuggestion = dispatchInput => { - return renderSuggestion(dispatchInput, '{}', 'JSON'); - }; - const renderValue = (dispatchFunc, value, prefix, valueType) => { const dispatchInput = val => { let _val = val; @@ -448,6 +440,14 @@ class PermissionBuilder extends React.Component { return renderInput(dispatchInput, value); }; + const sessionVariableSuggestion = () => { + return renderSuggestion(dispatchInput, 'X-Hasura-User-Id'); + }; + + const jsonSuggestion = () => { + return renderSuggestion(dispatchInput, '{}', 'JSON'); + }; + let input; let suggestion; @@ -459,10 +459,10 @@ class PermissionBuilder extends React.Component { PGTypes.geography.includes(valueType) ) { input = inputBox(); - suggestion = jsonSuggestion(dispatchInput); + suggestion = jsonSuggestion(); } else { input = wrapDoubleQuotes(inputBox()); - suggestion = sessionVariableSuggestion(dispatchInput); + suggestion = sessionVariableSuggestion(); } return ( @@ -477,6 +477,10 @@ class PermissionBuilder extends React.Component { dispatchFunc({ prefix: prefix, value: val }); }; + const sessionVariableSuggestion = () => { + return renderSuggestion(dispatchInput, 'X-Hasura-Allowed-Ids'); + }; + const inputArray = []; (values || []).concat(['']).map((val, i) => { From e362ee84a6f9855b22171b00b37bd7967264547c Mon Sep 17 00:00:00 2001 From: Nizar Malangadan Date: Tue, 26 Mar 2019 20:21:20 +0530 Subject: [PATCH 6/8] Add support for has_keys_any and has_keys_all in RQL --- .../src-lib/Hasura/GraphQL/Resolve/BoolExp.hs | 8 +++--- .../Hasura/RQL/DDL/Permission/Internal.hs | 6 ++-- server/src-lib/Hasura/RQL/GBoolExp.hs | 28 +++++++++++-------- server/src-lib/Hasura/RQL/Types.hs | 5 ++-- server/src-lib/Hasura/RQL/Types/BoolExp.hs | 17 +++++------ server/tests-py/context.py | 2 +- server/tests-py/test_v1_queries.py | 9 +++--- 7 files changed, 41 insertions(+), 34 deletions(-) diff --git a/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs b/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs index a1cd4f4677376..97c08c6c8e134 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/BoolExp.hs @@ -31,8 +31,8 @@ parseOpExps colTy annVal = do "_neq" -> fmap (ANE True) <$> asPGColValM v "_is_null" -> resolveIsNull v - "_in" -> fmap (AIN . catMaybes) <$> parseMany asPGColValM v - "_nin" -> fmap (ANIN . catMaybes) <$> parseMany asPGColValM v + "_in" -> fmap (AIN . Right . catMaybes) <$> parseMany asPGColValM v + "_nin" -> fmap (ANIN . Right . catMaybes) <$> parseMany asPGColValM v "_gt" -> fmap AGT <$> asPGColValM v "_lt" -> fmap ALT <$> asPGColValM v @@ -52,8 +52,8 @@ parseOpExps colTy annVal = do "_contains" -> fmap AContains <$> asPGColValM v "_contained_in" -> fmap AContainedIn <$> asPGColValM v "_has_key" -> fmap AHasKey <$> asPGColValM v - "_has_keys_any" -> fmap AHasKeysAny <$> parseMany asPGColText v - "_has_keys_all" -> fmap AHasKeysAll <$> parseMany asPGColText v + "_has_keys_any" -> fmap (AHasKeysAny . Right . catMaybes) <$> parseMany asPGColValM v + "_has_keys_all" -> fmap (AHasKeysAll . Right . catMaybes) <$> parseMany asPGColValM v -- geometry/geography type related operators "_st_contains" -> fmap ASTContains <$> asPGColValM v diff --git a/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs b/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs index 920f12a0930b8..5ae42c013e65f 100644 --- a/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs +++ b/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs @@ -222,7 +222,7 @@ valueParser = ValueParser parseOne parseMany qualJsonArrElemsTxtF = QualifiedObject (SchemaName "pg_catalog") arrElemsTextF arrElemsTextF = FunctionName "json_array_elements_text" - selJsonArrElems colTy x = S.SESelect $ S.mkSelect + selJsonArrElems colTy x = S.mkSelect { S.selExtr = [flip S.Extractor Nothing $ withGeoVal colTy (S.SEIden $ toIden arrElemsTextF) `S.SETyAnn` colTyAnn] , S.selFrom = Just $ S.FromExp [S.mkFuncFromItem qualJsonArrElemsTxtF [x]] } @@ -230,12 +230,12 @@ valueParser = ValueParser parseOne parseMany parseMany columnType v = case v of (String t) - | isUserVar t -> return [selJsonArrElems columnType $ fromCurSess t PGJSON ] + | isUserVar t -> return $ Left $ selJsonArrElems columnType $ fromCurSess t PGJSON | otherwise -> throw400 UnexpectedPayload "Expected Array, encountered String" val -> do vals <- runAesonParser parseJSON val - indexedForM vals (parseOne columnType) + fmap Right $ indexedForM vals (parseOne columnType) injectDefaults :: QualifiedTable -> QualifiedTable -> Q.Query injectDefaults qv qt = diff --git a/server/src-lib/Hasura/RQL/GBoolExp.hs b/server/src-lib/Hasura/RQL/GBoolExp.hs index e0b7cb6d6bed2..7bbc53d4d784a 100644 --- a/server/src-lib/Hasura/RQL/GBoolExp.hs +++ b/server/src-lib/Hasura/RQL/GBoolExp.hs @@ -80,12 +80,10 @@ parseOpExp (ValueParser parseOneFn parseManyFn) fim (PGColInfo cn colTy _) (opSt "_has_key" -> jsonbOnlyOp $ AHasKey <$> parseWithTy PGText "$has_key" -> jsonbOnlyOp $ AHasKey <$> parseWithTy PGText - --FIXME:- Parse a session variable as text array values - --TODO:- Add following commented operators after fixing above said - -- "_has_keys_any" -> jsonbOnlyOp $ AHasKeysAny <$> parseVal - -- "$has_keys_any" -> jsonbOnlyOp $ AHasKeysAny <$> parseVal - -- "_has_keys_all" -> jsonbOnlyOp $ AHasKeysAll <$> parseVal - -- "$has_keys_all" -> jsonbOnlyOp $ AHasKeysAll <$> parseVal + "_has_keys_any" -> jsonbOnlyOp $ AHasKeysAny <$> parseManyWithTy PGText + "$has_keys_any" -> jsonbOnlyOp $ AHasKeysAny <$> parseManyWithTy PGText + "_has_keys_all" -> jsonbOnlyOp $ AHasKeysAll <$> parseManyWithTy PGText + "$has_keys_all" -> jsonbOnlyOp $ AHasKeysAll <$> parseManyWithTy PGText -- geometry types "_st_contains" -> parseGeometryOp ASTContains @@ -198,6 +196,7 @@ parseOpExp (ValueParser parseOneFn parseManyFn) fim (PGColInfo cn colTy _) (opSt throwError $ buildMsg ty [PGGeometry, PGGeography] parseWithTy ty = parseOneFn ty val + parseManyWithTy ty = parseManyFn ty val parseOne = parseOneFn colTy val parseMany = parseManyFn colTy val @@ -345,8 +344,8 @@ mkColCompExp qual lhsCol = \case AContains val -> S.BECompare S.SContains lhs val AContainedIn val -> S.BECompare S.SContainedIn lhs val AHasKey val -> S.BECompare S.SHasKey lhs val - AHasKeysAny keys -> S.BECompare S.SHasKeysAny lhs $ toTextArray keys - AHasKeysAll keys -> S.BECompare S.SHasKeysAll lhs $ toTextArray keys + AHasKeysAny keys -> S.BECompare S.SHasKeysAny lhs $ either (S.SESelect . arrAgg) toTextArray keys + AHasKeysAll keys -> S.BECompare S.SHasKeysAll lhs $ either (S.SESelect . arrAgg) toTextArray keys ASTContains val -> mkGeomOpBe "ST_Contains" val ASTCrosses val -> mkGeomOpBe "ST_Crosses" val @@ -372,14 +371,21 @@ mkColCompExp qual lhsCol = \case lhs = mkQCol lhsCol toTextArray arr = - S.SETyAnn (S.SEArray $ map (txtEncoder . PGValText) arr) S.textArrType + S.SEArray arr `S.SETyAnn` S.textArrType + + arrAgg (S.Select a [(S.Extractor fe alias)] from b c d e f g) + = (S.Select a [S.Extractor (doArrAgg fe) alias] from b c d e f g) + where doArrAgg v = S.SEFnApp "array_agg" [v] Nothing + --Return the same expression if the underlying expression has more than one extractor + arrAgg e = e mkGeomOpBe fn v = applySQLFn fn [lhs, v] applySQLFn f exps = S.BEExp $ S.SEFnApp f exps Nothing - handleEmptyIn [] = S.BELit False - handleEmptyIn vals = S.BEIN lhs vals + handleEmptyIn (Left s) = S.BEIN lhs [S.SESelect s] + handleEmptyIn (Right []) = S.BELit False + handleEmptyIn (Right vals) = S.BEIN lhs vals getColExpDeps :: QualifiedTable -> AnnBoolExpFld a -> [SchemaDependency] getColExpDeps tn = \case diff --git a/server/src-lib/Hasura/RQL/Types.hs b/server/src-lib/Hasura/RQL/Types.hs index 42b05a8d4559a..3e41e8dbce1ed 100644 --- a/server/src-lib/Hasura/RQL/Types.hs +++ b/server/src-lib/Hasura/RQL/Types.hs @@ -58,6 +58,7 @@ import Hasura.RQL.Types.SchemaCache as R import Hasura.RQL.Types.EventTrigger as R import Hasura.SQL.Types +import qualified Hasura.SQL.DML as S import qualified Hasura.GraphQL.Context as GC @@ -363,7 +364,7 @@ type HeaderObj = M.HashMap T.Text T.Text data ValueParser m a = ValueParser { vpParseOne :: PGColType -> Value -> m a - , vpParseMany :: PGColType -> Value -> m [a] + , vpParseMany :: PGColType -> Value -> m (Either S.Select [a]) } defaultValueParser @@ -372,4 +373,4 @@ defaultValueParser defaultValueParser parseOne = ValueParser parseOne parseMany where parseMany colTy val = do vals <- runAesonParser parseJSON val - indexedForM vals (parseOne colTy) + fmap Right $ indexedForM vals (parseOne colTy) diff --git a/server/src-lib/Hasura/RQL/Types/BoolExp.hs b/server/src-lib/Hasura/RQL/Types/BoolExp.hs index 4bb34a4f4c751..43fed166f3176 100644 --- a/server/src-lib/Hasura/RQL/Types/BoolExp.hs +++ b/server/src-lib/Hasura/RQL/Types/BoolExp.hs @@ -113,8 +113,8 @@ data OpExpG a = AEQ !Bool !a | ANE !Bool !a - | AIN ![a] - | ANIN ![a] + | AIN !(Either S.Select [a]) + | ANIN !(Either S.Select [a]) | AGT !a | ALT !a @@ -133,8 +133,8 @@ data OpExpG a | AContains !a | AContainedIn !a | AHasKey !a - | AHasKeysAny [Text] - | AHasKeysAll [Text] + | AHasKeysAny !(Either S.Select [a]) + | AHasKeysAll !(Either S.Select [a]) | ASTContains !a | ASTCrosses !a @@ -163,8 +163,8 @@ opExpToJPair f = \case AEQ _ a -> ("_eq", f a) ANE _ a -> ("_ne", f a) - AIN a -> ("_in", toJSON $ map f a) - ANIN a -> ("_nin", toJSON $ map f a) + AIN a -> ("_in", manyToJson a) + ANIN a -> ("_nin", manyToJson a) AGT a -> ("_gt", f a) ALT a -> ("_lt", f a) @@ -183,8 +183,8 @@ opExpToJPair f = \case AContains a -> ("_contains", f a) AContainedIn a -> ("_contained_in", f a) AHasKey a -> ("_has_key", f a) - AHasKeysAny a -> ("_has_keys_any", toJSON a) - AHasKeysAll a -> ("_has_keys_all", toJSON a) + AHasKeysAny a -> ("_has_keys_any", manyToJson a) + AHasKeysAll a -> ("_has_keys_all", manyToJson a) ASTContains a -> ("_st_contains", f a) ASTCrosses a -> ("_st_crosses", f a) @@ -205,6 +205,7 @@ opExpToJPair f = \case CLT a -> ("_clt", toJSON a) CGTE a -> ("_cgte", toJSON a) CLTE a -> ("_clte", toJSON a) + where manyToJson = either (toJSON . S.SESelect) $ toJSON . map f data AnnBoolExpFld a = AVCol !PGColInfo ![OpExpG a] diff --git a/server/tests-py/context.py b/server/tests-py/context.py index b8676fc27d33e..e4c96d448842b 100644 --- a/server/tests-py/context.py +++ b/server/tests-py/context.py @@ -178,7 +178,7 @@ def v1q(self, q, headers = {}): def v1q_f(self, fn): with open(fn) as f: - return self.v1q(yaml.load(f)) + return self.v1q(yaml.safe_load(f)) def teardown(self): self.http.close() diff --git a/server/tests-py/test_v1_queries.py b/server/tests-py/test_v1_queries.py index 7150f079585c7..cdcc251190a29 100644 --- a/server/tests-py/test_v1_queries.py +++ b/server/tests-py/test_v1_queries.py @@ -172,12 +172,11 @@ def test_select_article_author_jsonb_contains_latest(self, hge_ctx): def test_select_author_article_jsonb_contains_bestseller(self, hge_ctx): check_query_f(hge_ctx, self.dir() + '/select_author_article_jsonb_contains_bestseller.yaml') - # TODO:- Uncomment the following after adding has_keys_all and has_keys_any operators - # def test_select_product_jsonb_has_keys_all_ram_touchscreen(self, hge_ctx): - # check_query_f(hge_ctx, self.dir() + '/select_product_jsonb_has_keys_all_ram_touchscreen.yaml') + def test_select_product_jsonb_has_keys_all_ram_touchscreen(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + '/select_product_jsonb_has_keys_all_ram_touchscreen.yaml') - # def test_select_product_jsonb_has_keys_any_os_operating_system(self, hge_ctx): - # check_query_f(hge_ctx, self.dir() + '/select_product_jsonb_has_keys_any_os_operating_system.yaml') + def test_select_product_jsonb_has_keys_any_os_operating_system(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + '/select_product_jsonb_has_keys_any_os_operating_system.yaml') def test_select_product_jsonb_has_key_sim_type(self, hge_ctx): check_query_f(hge_ctx, self.dir() + '/select_product_jsonb_has_key_sim_type.yaml') From 5c26c1ba5d4cce9d7b97ce743f7a58bfe5a56493 Mon Sep 17 00:00:00 2001 From: Nizar Malangadan Date: Wed, 27 Mar 2019 15:28:42 +0530 Subject: [PATCH 7/8] Tests far `has_keys_any` and `has_keys_all` --- .../developer_insert_has_keys_any_fail.yaml | 35 ++++++++++++ .../developer_insert_has_keys_any_pass.yaml | 38 +++++++++++++ ...ler_insert_computer_has_keys_all_fail.yaml | 37 +++++++++++++ ...ler_insert_computer_has_keys_all_pass.yaml | 39 +++++++++++++ .../insert/permissions/setup.yaml | 55 +++++++++++++++++-- .../insert/permissions/teardown.yaml | 1 + .../graphql_query/permissions/setup.yaml | 26 +++++++++ server/tests-py/test_graphql_mutations.py | 12 ++++ 8 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 server/tests-py/queries/graphql_mutation/insert/permissions/developer_insert_has_keys_any_fail.yaml create mode 100644 server/tests-py/queries/graphql_mutation/insert/permissions/developer_insert_has_keys_any_pass.yaml create mode 100644 server/tests-py/queries/graphql_mutation/insert/permissions/seller_insert_computer_has_keys_all_fail.yaml create mode 100644 server/tests-py/queries/graphql_mutation/insert/permissions/seller_insert_computer_has_keys_all_pass.yaml diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/developer_insert_has_keys_any_fail.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/developer_insert_has_keys_any_fail.yaml new file mode 100644 index 0000000000000..c4e6c34be1552 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/developer_insert_has_keys_any_fail.yaml @@ -0,0 +1,35 @@ +description: Inserts computer specs with atleast one of the required keys +url: /v1alpha1/graphql +status: 400 +response: + errors: + - extensions: + path: $.selectionSet.insert_computer.args.objects + code: permission-error + message: Check constraint violation. insert check constraint failed +headers: + X-Hasura-Role: developer + X-Hasura-Spec-Keys: |- + ["display","memory","warranty"] +query: + variables: + spec: + processor: AMD + query: | + mutation insert_computer($spec: jsonb) { + insert_computer ( + objects: [ + { + name: "Computer1", + spec: $spec + }, + ], + ) { + affected_rows + returning { + id + name + spec + } + } + } diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/developer_insert_has_keys_any_pass.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/developer_insert_has_keys_any_pass.yaml new file mode 100644 index 0000000000000..32a3efbec13b2 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/developer_insert_has_keys_any_pass.yaml @@ -0,0 +1,38 @@ +description: Inserts computer specs with atleast one of the required keys +url: /v1alpha1/graphql +status: 200 +response: + data: + insert_computer: + affected_rows: 1 + returning: + - id: 1 + name: Computer1 + spec: + processor: AMD +headers: + X-Hasura-Role: developer + X-Hasura-Spec-Keys: |- + ["processor","display","memory","warranty"] +query: + variables: + spec: + processor: AMD + query: | + mutation insert_computer($spec: jsonb) { + insert_computer ( + objects: [ + { + name: "Computer1", + spec: $spec + }, + ], + ) { + affected_rows + returning { + id + name + spec + } + } + } diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/seller_insert_computer_has_keys_all_fail.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/seller_insert_computer_has_keys_all_fail.yaml new file mode 100644 index 0000000000000..34fc0e7096101 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/seller_insert_computer_has_keys_all_fail.yaml @@ -0,0 +1,37 @@ +description: Inserts computer specs without all the required keys (error) +url: /v1alpha1/graphql +status: 400 +response: + errors: + - extensions: + path: $.selectionSet.insert_computer.args.objects + code: permission-error + message: Check constraint violation. insert check constraint failed +headers: + X-Hasura-Role: seller + X-Hasura-Spec-Required-Keys: |- + ["processor","display","memory","warranty"] +query: + variables: + spec: + processor: AMD + display: '16" FHD' + memory: '16 GB DDR4' + query: | + mutation insert_computer($spec: jsonb) { + insert_computer ( + objects: [ + { + name: "Computer1", + spec: $spec + }, + ], + ) { + affected_rows + returning { + id + name + spec + } + } + } diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/seller_insert_computer_has_keys_all_pass.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/seller_insert_computer_has_keys_all_pass.yaml new file mode 100644 index 0000000000000..3fe6df4adfe6c --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/seller_insert_computer_has_keys_all_pass.yaml @@ -0,0 +1,39 @@ +description: Inserts computer specs with required keys +url: /v1alpha1/graphql +status: 200 +response: + data: + insert_computer: + affected_rows: 1 + returning: + - id: 1 + name: Computer1 + spec: &spec + processor: AMD + display: '16" FHD' + memory: '16 GB DDR4' +headers: + X-Hasura-Role: seller + X-Hasura-Spec-Required-Keys: |- + ["processor","display","memory"] +query: + variables: + spec: *spec + query: | + mutation insert_computer($spec: jsonb) { + insert_computer ( + objects: [ + { + name: "Computer1", + spec: $spec + }, + ], + ) { + affected_rows + returning { + id + name + spec + } + } + } diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/setup.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/setup.yaml index 0425c7349b151..ccd79bb390828 100644 --- a/server/tests-py/queries/graphql_mutation/insert/permissions/setup.yaml +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/setup.yaml @@ -6,8 +6,8 @@ args: args: sql: | create table author( - id serial primary key, - name text unique, + id serial primary key, + name text unique, bio text, is_registered boolean not null default false ); @@ -219,7 +219,7 @@ args: - content: Sample article content title: Article 3 author_id: 2 - + #Company insert permission for user - type: create_insert_permission args: @@ -362,7 +362,7 @@ args: args: name: blog schema: public - + - type: create_select_permission args: table: blog @@ -392,5 +392,52 @@ args: last_updated: 'NOW()' updated_by: X-Hasura-User-Id +- type: run_sql + args: + sql: | + CREATE TABLE computer ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + spec JSONB NOT NULL + ); + +- type: track_table + args: + name: computer + schema: public +- type: create_insert_permission + args: + table: computer + role: seller + permission: + check: + spec: + _has_keys_all: X-Hasura-Spec-Required-Keys + columns: '*' +- type: create_insert_permission + args: + table: computer + role: developer + permission: + check: + spec: + _has_keys_any: X-Hasura-Spec-Keys + columns: '*' + +- type: create_select_permission + args: + table: computer + role: seller + permission: + columns: '*' + filter: {} + +- type: create_select_permission + args: + table: computer + role: developer + permission: + columns: '*' + filter: {} diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/teardown.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/teardown.yaml index d5f4c4ac63e0a..1cdbefb839843 100644 --- a/server/tests-py/queries/graphql_mutation/insert/permissions/teardown.yaml +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/teardown.yaml @@ -17,4 +17,5 @@ args: drop table blog; drop table author; drop table "Company"; + drop table computer; cascade: true diff --git a/server/tests-py/queries/graphql_query/permissions/setup.yaml b/server/tests-py/queries/graphql_query/permissions/setup.yaml index acaa2904767ef..643930eb3b276 100644 --- a/server/tests-py/queries/graphql_query/permissions/setup.yaml +++ b/server/tests-py/queries/graphql_query/permissions/setup.yaml @@ -394,6 +394,32 @@ args: jsonb_col: $nin: X-Hasura-Protected-Jsons +# Using jsonb column and $has_keys_any operator +- type: create_select_permission + args: + table: jsonb_table + role: user4 + permission: + columns: + - id + - jsonb_col + filter: + jsonb_col: + $has_keys_any: X-Hasura-Json-Keys + +# Using jsonb column and $has_keys_all operator +- type: create_select_permission + args: + table: jsonb_table + role: user5 + permission: + columns: + - id + - jsonb_col + filter: + jsonb_col: + $has_keys_all: X-Hasura-Json-Required-Keys + #Insert data - type: insert args: diff --git a/server/tests-py/test_graphql_mutations.py b/server/tests-py/test_graphql_mutations.py index dfba821cd0eff..ccaac0c0b2502 100644 --- a/server/tests-py/test_graphql_mutations.py +++ b/server/tests-py/test_graphql_mutations.py @@ -137,6 +137,18 @@ def test_arr_sess_var_insert_article_as_editor_allowed_user_id(self, hge_ctx): def test_arr_sess_var_insert_article_as_editor_err_not_allowed_user_id(self, hge_ctx): check_query_f(hge_ctx, self.dir() + "/insert_article_arr_sess_var_editors_err_not_allowed_user_id.yaml") + def test_seller_insert_computer_json_has_keys_all(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/seller_insert_computer_has_keys_all_pass.yaml") + + def test_seller_insert_computer_json_has_keys_all_err(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/seller_insert_computer_has_keys_all_fail.yaml") + + def test_developer_insert_computer_json_has_keys_any(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/developer_insert_has_keys_any_pass.yaml") + + def test_developer_insert_computer_json_has_keys_any_err(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/developer_insert_has_keys_any_fail.yaml") + @classmethod def dir(cls): return "queries/graphql_mutation/insert/permissions" From 06abe03e15035d8e6272dec58314c657fbf08be7 Mon Sep 17 00:00:00 2001 From: rikinsk Date: Mon, 15 Apr 2019 17:34:32 +0530 Subject: [PATCH 8/8] update docs --- .../schema-metadata-api/index.rst | 2 +- .../schema-metadata-api/syntax-defs.rst | 22 ++++++++++++------- docs/graphql/manual/auth/basics.rst | 2 -- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/graphql/manual/api-reference/schema-metadata-api/index.rst b/docs/graphql/manual/api-reference/schema-metadata-api/index.rst index 699a448b18134..283aa27c12618 100644 --- a/docs/graphql/manual/api-reference/schema-metadata-api/index.rst +++ b/docs/graphql/manual/api-reference/schema-metadata-api/index.rst @@ -221,5 +221,5 @@ Error codes Relationships Permissions Event Triggers - Syntax definitions + Common syntax definitions diff --git a/docs/graphql/manual/api-reference/schema-metadata-api/syntax-defs.rst b/docs/graphql/manual/api-reference/schema-metadata-api/syntax-defs.rst index 792b80bc3d9c2..9da35a3299be6 100644 --- a/docs/graphql/manual/api-reference/schema-metadata-api/syntax-defs.rst +++ b/docs/graphql/manual/api-reference/schema-metadata-api/syntax-defs.rst @@ -1,5 +1,5 @@ -Schema/Metadata API reference: Syntax definitions -================================================= +Schema/Metadata API Reference: Common syntax definitions +======================================================== .. contents:: Table of contents :backlinks: none @@ -248,7 +248,7 @@ ColumnExp Operator ^^^^^^^^ -Generic operators (all column types except json, jsonb) : +**Generic operators (all column types except json, jsonb) :** - ``"$eq"`` - ``"$ne"`` @@ -259,7 +259,7 @@ Generic operators (all column types except json, jsonb) : - ``"$gte"`` - ``"$lte"`` -Text related operators : +**Text related operators :** - ``"$like"`` - ``"$nlike"`` @@ -268,7 +268,7 @@ Text related operators : - ``"$similar"`` - ``"$nsimilar"`` -Operators for comparing columns (all column types except json, jsonb): +**Operators for comparing columns (all column types except json, jsonb):** - ``"$ceq"`` - ``"$cne"`` @@ -277,11 +277,11 @@ Operators for comparing columns (all column types except json, jsonb): - ``"$cgte"`` - ``"$clte"`` -Checking for NULL values : +**Checking for NULL values :** - ``_is_null`` (takes true/false as values) -JSONB operators : +**JSONB operators :** .. list-table:: :header-rows: 1 @@ -294,8 +294,14 @@ JSONB operators : - ``<@`` * - ``_has_key`` - ``?`` + * - ``_has_keys_any`` + - ``?|`` + * - ``_has_keys_all`` + - ``?&`` -PostGIS related operators on GEOMETRY columns: +(For more details on what these operators do, refer to `Postgres docs `__.) + +**PostGIS related operators on GEOMETRY columns:** .. list-table:: :header-rows: 1 diff --git a/docs/graphql/manual/auth/basics.rst b/docs/graphql/manual/auth/basics.rst index 762d42f617c81..527bf2bdb17c6 100644 --- a/docs/graphql/manual/auth/basics.rst +++ b/docs/graphql/manual/auth/basics.rst @@ -109,8 +109,6 @@ You can notice above how the same query now only includes the right slice of dat This rule reads as: allow selecting an article if it was published after "31-12-2018" and its author is the current user. - **Note:** The operators ``_has_keys_all`` and ``_has_keys_any`` are currently not supported in permission rules - .. _restrict_columns: Restrict access to certain columns