allow specifying a list of columns that can be inserted (close #250) (#917)

This commit is contained in:
Rakesh Emmadi
2018-11-02 20:38:38 +05:30
committed by Vamshi Surabhi
parent fbcae53efa
commit 999580481c
15 changed files with 322 additions and 174 deletions

View File

@@ -41,6 +41,7 @@ const defaultQueryPermissions = {
check: {},
allow_upsert: true,
set: {},
columns: [],
localSet: [
{
...defaultInsertSetState,

View File

@@ -279,7 +279,8 @@ const modifyReducer = (tableName, schemas, modifyStateOrig, action) => {
...getBasePermissionsState(
action.tableSchema,
action.role,
action.query
action.query,
action.insertPermColumnRestriction
),
},
};

View File

@@ -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,

View File

@@ -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 ? (
<input
onChange={dispatchBulkSelect}
checked="checked"
data-role={role}
className={styles.bulkSelect}
type="checkbox"
/>
) : (
<input
onChange={dispatchBulkSelect}
data-role={role}
className={styles.bulkSelect}
type="checkbox"
/>
);
<input
onChange={dispatchBulkSelect}
checked="checked"
data-role={role}
className={styles.bulkSelect}
type="checkbox"
/>
) : (
<input
onChange={dispatchBulkSelect}
data-role={role}
className={styles.bulkSelect}
type="checkbox"
/>
);
_permissionsRowHtml.push(
<td key={-1}>
<div>
@@ -765,164 +772,164 @@ class Permissions extends Component {
const setOptions =
insertState && insertState.localSet && insertState.localSet.length > 0
? insertState.localSet.map((s, i) => {
return (
<div className={styles.insertSetConfigRow} key={i}>
<div
className={
styles.display_inline +
return (
<div className={styles.insertSetConfigRow} key={i}>
<div
className={
styles.display_inline +
' ' +
styles.add_mar_right +
' ' +
styles.input_element_wrapper
}
>
<select
className="input-sm form-control"
value={s.key}
onChange={this.onSetKeyChange.bind(this)}
data-index-id={i}
disabled={disableInput}
}
>
<option value="" disabled>
<select
className="input-sm form-control"
value={s.key}
onChange={this.onSetKeyChange.bind(this)}
data-index-id={i}
disabled={disableInput}
>
<option value="" disabled>
Column Name
</option>
{columns && columns.length > 0
? columns.map((c, key) => (
<option
value={c.column_name}
data-column-type={c.data_type}
key={key}
>
{c.column_name}
</option>
))
: null}
</select>
</div>
<div
className={
styles.display_inline +
</option>
{columns && columns.length > 0
? columns.map((c, key) => (
<option
value={c.column_name}
data-column-type={c.data_type}
key={key}
>
{c.column_name}
</option>
))
: null}
</select>
</div>
<div
className={
styles.display_inline +
' ' +
styles.add_mar_right +
' ' +
styles.input_element_wrapper
}
>
<select
className="input-sm form-control"
onChange={this.onSetTypeChange.bind(this)}
data-index-id={i}
value={setConfigValueType(s.value) || ''}
disabled={disableInput}
}
>
<option value="" disabled>
<select
className="input-sm form-control"
onChange={this.onSetTypeChange.bind(this)}
data-index-id={i}
value={setConfigValueType(s.value) || ''}
disabled={disableInput}
>
<option value="" disabled>
Select Preset Type
</option>
<option value="static">static</option>
<option value="session">from session variable</option>
</select>
</div>
<div
className={
styles.display_inline +
</option>
<option value="static">static</option>
<option value="session">from session variable</option>
</select>
</div>
<div
className={
styles.display_inline +
' ' +
styles.add_mar_right +
' ' +
styles.input_element_wrapper
}
>
{setConfigValueType(s.value) === 'session' ? (
<InputGroup>
<InputGroup.Addon>X-Hasura-</InputGroup.Addon>
<input
className={'input-sm form-control '}
}
>
{setConfigValueType(s.value) === 'session' ? (
<InputGroup>
<InputGroup.Addon>X-Hasura-</InputGroup.Addon>
<input
className={'input-sm form-control '}
placeholder="column_value"
value={s.value.slice(X_HASURA_CONST.length)}
onChange={this.onSetValueChange.bind(this)}
onBlur={e => this.onSetValueBlur(e, i, null)}
data-index-id={i}
data-prefix-val={X_HASURA_CONST}
disabled={disableInput}
/>
</InputGroup>
) : (
<EnhancedInput
placeholder="column_value"
value={s.value.slice(X_HASURA_CONST.length)}
type={
i in this.state.insertSetOperations.columnTypeMap
? this.state.insertSetOperations.columnTypeMap[i]
: ''
}
value={s.value}
onChange={this.onSetValueChange.bind(this)}
onBlur={e => this.onSetValueBlur(e, i, null)}
data-index-id={i}
onBlur={this.onSetValueBlur}
indexId={i}
data-prefix-val={X_HASURA_CONST}
disabled={disableInput}
/>
</InputGroup>
) : (
<EnhancedInput
placeholder="column_value"
type={
i in this.state.insertSetOperations.columnTypeMap
? this.state.insertSetOperations.columnTypeMap[i]
: ''
)}
</div>
{setConfigValueType(s.value) === 'session' ? (
<div
className={
styles.display_inline +
' ' +
styles.add_mar_right +
' ' +
styles.input_element_wrapper +
' ' +
styles.e_g_text
}
>
e.g. X-Hasura-User-Id
</div>
) : (
<div
className={
styles.display_inline +
' ' +
styles.add_mar_right +
' ' +
styles.input_element_wrapper +
' ' +
styles.e_g_text
}
>
e.g. false, 1, some-text
</div>
)}
{i !== insertState.localSet.length - 1 ? (
<div
className={
styles.display_inline +
' ' +
styles.add_mar_right +
' ' +
styles.input_element_wrapper
}
>
<i
className="fa-lg fa fa-times"
onClick={
!disableInput ? this.deleteSetKeyVal.bind(this) : ''
}
data-index-id={i}
/>
</div>
) : (
<div
className={
styles.display_inline +
' ' +
styles.add_mar_right +
' ' +
styles.input_element_wrapper
}
value={s.value}
onChange={this.onSetValueChange.bind(this)}
onBlur={this.onSetValueBlur}
indexId={i}
data-prefix-val={X_HASURA_CONST}
disabled={disableInput}
/>
)}
</div>
{setConfigValueType(s.value) === 'session' ? (
<div
className={
styles.display_inline +
' ' +
styles.add_mar_right +
' ' +
styles.input_element_wrapper +
' ' +
styles.e_g_text
}
>
e.g. X-Hasura-User-Id
</div>
) : (
<div
className={
styles.display_inline +
' ' +
styles.add_mar_right +
' ' +
styles.input_element_wrapper +
' ' +
styles.e_g_text
}
>
e.g. false, 1, some-text
</div>
)}
{i !== insertState.localSet.length - 1 ? (
<div
className={
styles.display_inline +
' ' +
styles.add_mar_right +
' ' +
styles.input_element_wrapper
}
>
<i
className="fa-lg fa fa-times"
onClick={
!disableInput ? this.deleteSetKeyVal.bind(this) : ''
}
data-index-id={i}
/>
</div>
) : (
<div
className={
styles.display_inline +
' ' +
styles.add_mar_right +
' ' +
styles.input_element_wrapper
}
/>
)}
</div>
);
})
);
})
: 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 = (
<div className={styles.editPermissionsSection}>
<div>

View File

@@ -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 => {

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -117,6 +117,12 @@ class TestGraphqlInsertPermission(DefaultTestQueries):
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"