diff --git a/console/src/components/Services/Data/DataState.js b/console/src/components/Services/Data/DataState.js index 87bfd05891a95..5ab48727dcd4d 100644 --- a/console/src/components/Services/Data/DataState.js +++ b/console/src/components/Services/Data/DataState.js @@ -41,6 +41,7 @@ const defaultQueryPermissions = { check: {}, allow_upsert: true, set: {}, + columns: [], localSet: [ { ...defaultInsertSetState, diff --git a/console/src/components/Services/Data/TableCommon/TableReducer.js b/console/src/components/Services/Data/TableCommon/TableReducer.js index 7c28151e1ba27..961f2d3ab7750 100644 --- a/console/src/components/Services/Data/TableCommon/TableReducer.js +++ b/console/src/components/Services/Data/TableCommon/TableReducer.js @@ -279,7 +279,8 @@ const modifyReducer = (tableName, schemas, modifyStateOrig, action) => { ...getBasePermissionsState( action.tableSchema, action.role, - action.query + action.query, + action.insertPermColumnRestriction ), }, }; diff --git a/console/src/components/Services/Data/TablePermissions/Actions.js b/console/src/components/Services/Data/TablePermissions/Actions.js index 5be8fd9bd719e..347aeac207663 100644 --- a/console/src/components/Services/Data/TablePermissions/Actions.js +++ b/console/src/components/Services/Data/TablePermissions/Actions.js @@ -49,17 +49,29 @@ export const SET_TYPE_CONFIG = 'ModifyTable/SET_TYPE_CONFIG'; /* */ -const queriesWithPermColumns = ['select', 'update']; +const getQueriesWithPermColumns = insert => { + const queries = ['select', 'update']; + if (insert) { + queries.push('insert'); + } + return queries; +}; const permChangeTypes = { save: 'update', delete: 'delete', }; -const permOpenEdit = (tableSchema, role, query) => ({ +const permOpenEdit = ( + tableSchema, + role, + query, + insertPermColumnRestriction +) => ({ type: PERM_OPEN_EDIT, tableSchema, role, query, + insertPermColumnRestriction, }); const permSetFilter = filter => ({ type: PERM_SET_FILTER, filter }); const permSetFilterSameAs = filter => ({ @@ -122,7 +134,12 @@ const setConfigValueType = value => { : 'static'; }; -const getBasePermissionsState = (tableSchema, role, query) => { +const getBasePermissionsState = ( + tableSchema, + role, + query, + insertPermColumnRestriction +) => { const _permissions = JSON.parse(JSON.stringify(defaultPermissionsState)); _permissions.table = tableSchema.table_name; @@ -139,6 +156,13 @@ const getBasePermissionsState = (tableSchema, role, query) => { // If the query is insert, transform set object if exists to an array if (q === 'insert') { // If set is an object + if (insertPermColumnRestriction) { + if (!_permissions[q].columns) { + _permissions[q].columns = tableSchema.columns.map( + c => c.column_name + ); + } + } if ('set' in _permissions[q]) { if ( Object.keys(_permissions[q].set).length > 0 && @@ -619,7 +643,7 @@ export { permSetBulkSelect, toggleColumn, toggleAllColumns, - queriesWithPermColumns, + getQueriesWithPermColumns, getFilterKey, getBasePermissionsState, updatePermissionsState, diff --git a/console/src/components/Services/Data/TablePermissions/Permissions.js b/console/src/components/Services/Data/TablePermissions/Permissions.js index a23117f3b3698..bfd04d55ec7a2 100644 --- a/console/src/components/Services/Data/TablePermissions/Permissions.js +++ b/console/src/components/Services/Data/TablePermissions/Permissions.js @@ -9,7 +9,7 @@ import 'brace/theme/github'; import { RESET } from '../TableModify/ModifyActions'; import { - queriesWithPermColumns, + getQueriesWithPermColumns, permChangeTypes, permOpenEdit, permSetFilter, @@ -417,7 +417,14 @@ class Permissions extends Component { if (isNewPerm && permsState.newRole !== '') { dispatch(permOpenEdit(tableSchema, permsState.newRole, queryType)); } else if (role !== '') { - dispatch(permOpenEdit(tableSchema, role, queryType)); + dispatch( + permOpenEdit( + tableSchema, + role, + queryType, + semverCheck('insertPermRestrictColumns', this.props.serverVersion) + ) + ); } else { window.alert('Please enter a role name'); } @@ -452,21 +459,21 @@ class Permissions extends Component { const bulkSelect = permsState.bulkSelect; const currentInputSelection = bulkSelect.filter(e => e === role) .length ? ( - - ) : ( - - ); + + ) : ( + + ); _permissionsRowHtml.push(
@@ -765,164 +772,164 @@ class Permissions extends Component { const setOptions = insertState && insertState.localSet && insertState.localSet.length > 0 ? insertState.localSet.map((s, i) => { - return ( -
-
+
- + - {columns && columns.length > 0 - ? columns.map((c, key) => ( - - )) - : null} - -
-
+ {columns && columns.length > 0 + ? columns.map((c, key) => ( + + )) + : null} + +
+
- + - - - -
-
+ + + +
+
- {setConfigValueType(s.value) === 'session' ? ( - - X-Hasura- - + {setConfigValueType(s.value) === 'session' ? ( + + X-Hasura- + this.onSetValueBlur(e, i, null)} + data-index-id={i} + data-prefix-val={X_HASURA_CONST} + disabled={disableInput} + /> + + ) : ( + this.onSetValueBlur(e, i, null)} - data-index-id={i} + onBlur={this.onSetValueBlur} + indexId={i} data-prefix-val={X_HASURA_CONST} disabled={disableInput} /> - - ) : ( - - )} -
- {setConfigValueType(s.value) === 'session' ? ( -
+ {setConfigValueType(s.value) === 'session' ? ( +
+ } + > e.g. X-Hasura-User-Id -
- ) : ( -
+ ) : ( +
+ } + > e.g. false, 1, some-text -
- )} - {i !== insertState.localSet.length - 1 ? ( -
+ )} + {i !== insertState.localSet.length - 1 ? ( +
- -
- ) : ( -
+ +
+ ) : ( +
- )} -
- ); - }) + } + /> + )} +
+ ); + }) : null; return ( @@ -1056,13 +1063,15 @@ class Permissions extends Component { const getColumnSection = (tableSchema, permsState) => { let _columnSection = ''; const query = permsState.query; - - if (queriesWithPermColumns.indexOf(query) !== -1) { + if ( + getQueriesWithPermColumns( + semverCheck('insertPermRestrictColumns', this.props.serverVersion) + ).indexOf(query) !== -1 + ) { const dispatchToggleAllColumns = () => { const allColumns = tableSchema.columns.map(c => c.column_name); dispatch(permToggleAllColumns(allColumns)); }; - _columnSection = (
diff --git a/console/src/helpers/semver.js b/console/src/helpers/semver.js index f78946847682f..5bfed163d26e4 100644 --- a/console/src/helpers/semver.js +++ b/console/src/helpers/semver.js @@ -9,6 +9,7 @@ const componentsSemver = { supportColumnChangeTrigger: '1.0.0-alpha26', analyzeApiChange: '1.0.0-alpha26', insertPrefix: '1.0.0-alpha26', + insertPermRestrictColumns: '1.0.0-alpha28', }; const getPreRelease = version => { diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs b/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs index ac85f4cfbf15d..d967e51f414a4 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Insert.hs @@ -504,8 +504,7 @@ resolveInsCtx tn = do InsCtx view colInfos setVals relInfoMap <- onNothing (Map.lookup tn ctxMap) $ throw500 $ "table " <> tn <<> " not found" - let defValMap = Map.fromList $ flip zip (repeat $ S.SEUnsafe "DEFAULT") $ - map pgiName colInfos + let defValMap = S.mkColDefValMap $ map pgiName colInfos defValWithSet = Map.union setVals defValMap return (view, colInfos, defValWithSet, relInfoMap) diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs b/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs index 97220546f5fba..9ee340ba3e56e 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Mutation.hs @@ -11,7 +11,6 @@ module Hasura.GraphQL.Resolve.Mutation import Hasura.Prelude -import qualified Data.HashMap.Strict as Map import qualified Data.HashMap.Strict.InsOrd as OMap import qualified Language.GraphQL.Draft.Syntax as G @@ -43,15 +42,14 @@ convertMutResp ty selSet = convertRowObj :: (MonadError QErr m, MonadState PrepArgs m) - => InsSetCols -> AnnGValue + => AnnGValue -> m [(PGCol, S.SQLExp)] -convertRowObj setVals val = - flip withObject val $ \_ obj -> do - inpVals <- forM (OMap.toList obj) $ \(k, v) -> do +convertRowObj val = + flip withObject val $ \_ obj -> + forM (OMap.toList obj) $ \(k, v) -> do prepExpM <- asPGColValM v >>= mapM prepare let prepExp = fromMaybe (S.SEUnsafe "NULL") prepExpM return (PGCol $ G.unName k, prepExp) - return $ Map.toList setVals <> inpVals type ApplySQLOp = (PGCol, S.SQLExp) -> S.SQLExp @@ -98,7 +96,7 @@ convertUpdate -> Convert RespTx convertUpdate tn filterExp fld = do -- a set expression is same as a row object - setExpM <- withArgM args "_set" $ convertRowObj Map.empty + setExpM <- withArgM args "_set" convertRowObj -- where bool expression to filter column whereExp <- withArg args "where" $ convertBoolExp tn -- increment operator on integer columns diff --git a/server/src-lib/Hasura/RQL/DDL/Permission.hs b/server/src-lib/Hasura/RQL/DDL/Permission.hs index 338c56098233c..72d75862964ed 100644 --- a/server/src-lib/Hasura/RQL/DDL/Permission.hs +++ b/server/src-lib/Hasura/RQL/DDL/Permission.hs @@ -74,6 +74,7 @@ data InsPerm { icCheck :: !BoolExp , icAllowUpsert :: !(Maybe Bool) , icSet :: !(Maybe Object) + , icColumns :: !(Maybe PermColSpec) } deriving (Show, Eq, Lift) $(deriveJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''InsPerm) @@ -109,7 +110,7 @@ buildInsPermInfo => TableInfo -> PermDef InsPerm -> m InsPermInfo -buildInsPermInfo tabInfo (PermDef rn (InsPerm chk upsrt set) _) = withPathK "permission" $ do +buildInsPermInfo tabInfo (PermDef rn (InsPerm chk upsrt set mCols) _) = withPathK "permission" $ do (be, beDeps) <- withPathK "check" $ procBoolExp tn fieldInfoMap (S.QualVar "NEW") chk let deps = mkParentDep tn : beDeps @@ -125,11 +126,17 @@ buildInsPermInfo tabInfo (PermDef rn (InsPerm chk upsrt set) _) = withPathK "per return (pgCol, sqlExp) let setHdrs = mapMaybe (fetchHdr . snd) (HM.toList setObj) reqHdrs = fltrHeaders `union` setHdrs - return $ InsPermInfo vn be allowUpsrt setColsSQL deps reqHdrs + preSetCols = HM.union setColsSQL nonInsColVals + return $ InsPermInfo vn be allowUpsrt preSetCols deps reqHdrs where fieldInfoMap = tiFieldInfoMap tabInfo tn = tiName tabInfo vn = buildViewName tn rn PTInsert + allCols = map pgiName $ getCols fieldInfoMap + nonInsCols = case mCols of + Nothing -> [] + Just cols -> (\\) allCols $ convColSpec fieldInfoMap cols + nonInsColVals = S.mkColDefValMap nonInsCols fetchHdr (String t) = bool Nothing (Just $ T.toLower t) $ isUserVar t diff --git a/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs b/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs index b089847e897e8..438837de5dc11 100644 --- a/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs +++ b/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs @@ -47,9 +47,7 @@ instance ToJSON PermColSpec where convColSpec :: FieldInfoMap -> PermColSpec -> [PGCol] convColSpec _ (PCCols cols) = cols -convColSpec cim PCStar = - map pgiName $ fst $ partitionEithers $ - map fieldInfoToEither $ M.elems cim +convColSpec cim PCStar = map pgiName $ getCols cim assertPermNotDefined :: (MonadError QErr m) diff --git a/server/src-lib/Hasura/RQL/DML/Insert.hs b/server/src-lib/Hasura/RQL/DML/Insert.hs index 1bee82e82a4a7..ea3e39b4c0722 100644 --- a/server/src-lib/Hasura/RQL/DML/Insert.hs +++ b/server/src-lib/Hasura/RQL/DML/Insert.hs @@ -86,9 +86,11 @@ convObj -> InsObj -> m ([PGCol], [S.SQLExp]) convObj prepFn defInsVals setInsVals fieldInfoMap insObj = do - inpInsVals <- flip HM.traverseWithKey reqInsObj $ \c val -> do + inpInsVals <- flip HM.traverseWithKey insObj $ \c val -> do let relWhenPGErr = "relationships can't be inserted" colType <- askPGType fieldInfoMap c relWhenPGErr + -- if column has predefined value then throw error + when (c `elem` preSetCols) $ throwNotInsErr c -- Encode aeson's value into prepared value withPathK (getPGColTxt c) $ prepFn colType val let insVals = HM.union setInsVals inpInsVals @@ -97,7 +99,12 @@ convObj prepFn defInsVals setInsVals fieldInfoMap insObj = do return (inpCols, sqlExps) where - reqInsObj = HM.difference insObj setInsVals + preSetCols = HM.keys setInsVals + + throwNotInsErr c = do + role <- userRole <$> askUserInfo + throw400 NotSupported $ "column " <> c <<> " is not insertable" + <> " for role " <>> role buildConflictClause :: (P1C m) diff --git a/server/src-lib/Hasura/SQL/DML.hs b/server/src-lib/Hasura/SQL/DML.hs index 182eddf85686d..59490ec37bbc9 100644 --- a/server/src-lib/Hasura/SQL/DML.hs +++ b/server/src-lib/Hasura/SQL/DML.hs @@ -12,6 +12,7 @@ import Hasura.SQL.Types import Data.String (fromString) import Language.Haskell.TH.Syntax (Lift) +import qualified Data.HashMap.Strict as HM import qualified Data.Text.Extended as T import qualified Text.Builder as TB @@ -318,6 +319,10 @@ mkSQLOpExp -> SQLExp -- result mkSQLOpExp op lhs rhs = SEOpApp op [lhs, rhs] +mkColDefValMap :: [PGCol] -> HM.HashMap PGCol SQLExp +mkColDefValMap cols = + HM.fromList $ zip cols (repeat $ SEUnsafe "DEFAULT") + handleIfNull :: SQLExp -> SQLExp -> SQLExp handleIfNull l e = SEFnApp "coalesce" [e, l] Nothing diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/resident_infant.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/resident_infant.yaml new file mode 100644 index 0000000000000..0e942b37e58ad --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/resident_infant.yaml @@ -0,0 +1,35 @@ +description: Insert into resident table only with insertable columns +url: /v1alpha1/graphql +status: 200 +headers: + X-Hasura-Role: infant + X-Hasura-Infant-Id: '1' + X-Hasura-Infant-Name: 'Bittu' +response: + data: + insert_resident: + affected_rows: 1 + returning: + - id: 1 + name: Bittu + age: 3 + is_user: false +query: + query: | + mutation { + insert_resident( + objects: [ + { + age: 3 + } + ] + ){ + affected_rows + returning{ + id + name + age + is_user + } + } + } diff --git a/server/tests-py/queries/graphql_mutation/insert/permissions/resident_infant_fail.yaml b/server/tests-py/queries/graphql_mutation/insert/permissions/resident_infant_fail.yaml new file mode 100644 index 0000000000000..a8b12d33a93be --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/resident_infant_fail.yaml @@ -0,0 +1,27 @@ +description: Insert into resident table with non insertable columns (Error) +url: /v1alpha1/graphql +status: 400 +headers: + X-Hasura-Role: infant + X-Hasura-Infant-Id: '1' + X-Hasura-Infant-Name: 'Bittu' +query: + query: | + mutation { + insert_resident( + objects: [ + { + age: 3 + is_user: true + } + ] + ){ + affected_rows + returning{ + id + name + age + is_user + } + } + } 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 550600f992412..1154044035ddd 100644 --- a/server/tests-py/queries/graphql_mutation/insert/permissions/setup.yaml +++ b/server/tests-py/queries/graphql_mutation/insert/permissions/setup.yaml @@ -222,3 +222,33 @@ args: - is_user filter: id: X-Hasura-Resident-Id + +#Create insert permission for infant on resident +- type: create_insert_permission + args: + table: resident + role: infant + permission: + check: + id: X-Hasura-Infant-Id + allow_upsert: false + set: + name: X-Hasura-Infant-Name + id: X-Hasura-Infant-Id + columns: + - age + +#Create select permission for infant on resident +- type: create_select_permission + args: + table: resident + role: infant + permission: + columns: + - id + - name + - age + - is_user + filter: + id: X-Hasura-Infant-Id + diff --git a/server/tests-py/test_graphql_mutations.py b/server/tests-py/test_graphql_mutations.py index 886ef7172ed87..354602ac68132 100644 --- a/server/tests-py/test_graphql_mutations.py +++ b/server/tests-py/test_graphql_mutations.py @@ -117,6 +117,12 @@ def test_company_user_role_insert_on_conflict(self, hge_ctx): def test_resident_user_role_insert(self, hge_ctx): check_query_f(hge_ctx, self.dir() + "/resident_user.yaml") + def test_resident_infant_role_insert(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/resident_infant.yaml") + + def test_resident_infant_role_insert_fail(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + "/resident_infant_fail.yaml") + @classmethod def dir(cls): return "queries/graphql_mutation/insert/permissions"