mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 20:41:49 +03:00
3247c8bd71
https://github.com/hasura/graphql-engine-mono/pull/2270 GitOrigin-RevId: d7644b25d3ee57ffa630de15ae692c1bfa03b4f6
365 lines
13 KiB
Haskell
365 lines
13 KiB
Haskell
module Hasura.RQL.DML.Internal where
|
|
|
|
import Hasura.Prelude
|
|
|
|
import qualified Data.HashMap.Strict as M
|
|
import qualified Data.HashSet as HS
|
|
import qualified Data.Sequence as DS
|
|
import qualified Data.Text as T
|
|
import qualified Database.PG.Query as Q
|
|
|
|
import Control.Lens
|
|
import Data.Aeson.Types
|
|
import Data.Text.Extended
|
|
|
|
import qualified Hasura.Backends.Postgres.SQL.DML as S
|
|
|
|
import Hasura.Backends.Postgres.SQL.Error
|
|
import Hasura.Backends.Postgres.SQL.Types hiding (TableName)
|
|
import Hasura.Backends.Postgres.SQL.Value
|
|
import Hasura.Backends.Postgres.Translate.BoolExp
|
|
import Hasura.Backends.Postgres.Translate.Column
|
|
import Hasura.Backends.Postgres.Types.Column
|
|
import Hasura.Base.Error
|
|
import Hasura.RQL.Types
|
|
import Hasura.SQL.Types
|
|
import Hasura.Session
|
|
|
|
|
|
newtype DMLP1T m a
|
|
= DMLP1T { unDMLP1T :: StateT (DS.Seq Q.PrepArg) m a }
|
|
deriving ( Functor, Applicative, Monad, MonadTrans
|
|
, MonadState (DS.Seq Q.PrepArg), MonadError e
|
|
, SourceM, TableCoreInfoRM b, TableInfoRM b, CacheRM, UserInfoM, HasServerConfigCtx
|
|
)
|
|
|
|
runDMLP1T :: DMLP1T m a -> m (a, DS.Seq Q.PrepArg)
|
|
runDMLP1T = flip runStateT DS.empty . unDMLP1T
|
|
|
|
mkAdminRolePermInfo :: Backend b => TableCoreInfo b -> RolePermInfo b
|
|
mkAdminRolePermInfo ti =
|
|
RolePermInfo (Just i) (Just s) (Just u) (Just d)
|
|
where
|
|
fields = _tciFieldInfoMap ti
|
|
pgCols = map pgiColumn $ getCols fields
|
|
pgColsWithFilter = M.fromList $ map (, Nothing) pgCols
|
|
scalarComputedFields =
|
|
HS.fromList $ map _cfiName $ onlyScalarComputedFields $ getComputedFieldInfos fields
|
|
scalarComputedFields' = HS.toMap scalarComputedFields $> Nothing
|
|
|
|
|
|
tn = _tciName ti
|
|
i = InsPermInfo (HS.fromList pgCols) annBoolExpTrue M.empty False mempty
|
|
s = SelPermInfo pgColsWithFilter scalarComputedFields' annBoolExpTrue Nothing True mempty
|
|
u = UpdPermInfo (HS.fromList pgCols) tn annBoolExpTrue Nothing M.empty mempty
|
|
d = DelPermInfo tn annBoolExpTrue mempty
|
|
|
|
askPermInfo'
|
|
:: (UserInfoM m, Backend b)
|
|
=> PermAccessor b c
|
|
-> TableInfo b
|
|
-> m (Maybe c)
|
|
askPermInfo' pa tableInfo = do
|
|
role <- askCurRole
|
|
return $ getPermInfoMaybe role pa tableInfo
|
|
|
|
getPermInfoMaybe
|
|
:: (Backend b) => RoleName -> PermAccessor b c -> TableInfo b -> Maybe c
|
|
getPermInfoMaybe role pa tableInfo =
|
|
getRolePermInfo role tableInfo >>= (^. permAccToLens pa)
|
|
|
|
getRolePermInfo
|
|
:: Backend b => RoleName -> TableInfo b -> Maybe (RolePermInfo b)
|
|
getRolePermInfo role tableInfo
|
|
| role == adminRoleName =
|
|
Just $ mkAdminRolePermInfo (_tiCoreInfo tableInfo)
|
|
| otherwise =
|
|
M.lookup role (_tiRolePermInfoMap tableInfo)
|
|
|
|
askPermInfo
|
|
:: (UserInfoM m, QErrM m, Backend b)
|
|
=> PermAccessor b c
|
|
-> TableInfo b
|
|
-> m c
|
|
askPermInfo pa tableInfo = do
|
|
roleName <- askCurRole
|
|
mPermInfo <- askPermInfo' pa tableInfo
|
|
onNothing mPermInfo $ throw400 PermissionDenied $ mconcat
|
|
[ pt <> " on " <>> tableInfoName tableInfo
|
|
, " for role " <>> roleName
|
|
, " is not allowed. "
|
|
]
|
|
where
|
|
pt = permTypeToCode $ permAccToType pa
|
|
|
|
isTabUpdatable :: RoleName -> TableInfo ('Postgres pgKind) -> Bool
|
|
isTabUpdatable role ti
|
|
| role == adminRoleName = True
|
|
| otherwise = isJust $ M.lookup role rpim >>= _permUpd
|
|
where
|
|
rpim = _tiRolePermInfoMap ti
|
|
|
|
askInsPermInfo
|
|
:: (UserInfoM m, QErrM m, Backend b)
|
|
=> TableInfo b -> m (InsPermInfo b)
|
|
askInsPermInfo = askPermInfo PAInsert
|
|
|
|
askSelPermInfo
|
|
:: (UserInfoM m, QErrM m, Backend b)
|
|
=> TableInfo b -> m (SelPermInfo b)
|
|
askSelPermInfo = askPermInfo PASelect
|
|
|
|
askUpdPermInfo
|
|
:: (UserInfoM m, QErrM m, Backend b)
|
|
=> TableInfo b -> m (UpdPermInfo b)
|
|
askUpdPermInfo = askPermInfo PAUpdate
|
|
|
|
askDelPermInfo
|
|
:: (UserInfoM m, QErrM m, Backend b)
|
|
=> TableInfo b -> m (DelPermInfo b)
|
|
askDelPermInfo = askPermInfo PADelete
|
|
|
|
verifyAsrns :: (MonadError QErr m) => [a -> m ()] -> [a] -> m ()
|
|
verifyAsrns preds xs = indexedForM_ xs $ \a -> mapM_ ($ a) preds
|
|
|
|
checkSelOnCol
|
|
:: forall b m
|
|
. (UserInfoM m, QErrM m, Backend b)
|
|
=> SelPermInfo b
|
|
-> Column b
|
|
-> m ()
|
|
checkSelOnCol selPermInfo =
|
|
checkPermOnCol @b PTSelect (HS.fromList $ M.keys $ spiCols @b selPermInfo)
|
|
|
|
checkPermOnCol
|
|
:: forall b m
|
|
. (UserInfoM m, QErrM m, Backend b)
|
|
=> PermType
|
|
-> HS.HashSet (Column b)
|
|
-> Column b
|
|
-> m ()
|
|
checkPermOnCol pt allowedCols col = do
|
|
role <- askCurRole
|
|
unless (HS.member col allowedCols) $
|
|
throw400 PermissionDenied $ permErrMsg role
|
|
where
|
|
permErrMsg role
|
|
| role == adminRoleName = "no such column exists : " <>> col
|
|
| otherwise = mconcat
|
|
[ "role " <>> role
|
|
, " does not have permission to "
|
|
, permTypeToCode pt <> " column " <>> col
|
|
]
|
|
|
|
checkSelectPermOnScalarComputedField
|
|
:: forall b m
|
|
. (UserInfoM m, QErrM m)
|
|
=> SelPermInfo b
|
|
-> ComputedFieldName
|
|
-> m ()
|
|
checkSelectPermOnScalarComputedField selPermInfo computedField = do
|
|
role <- askCurRole
|
|
unless (M.member computedField $ spiScalarComputedFields selPermInfo) $
|
|
throw400 PermissionDenied $ permErrMsg role
|
|
where
|
|
permErrMsg role
|
|
| role == adminRoleName = "no such computed field exists : " <>> computedField
|
|
| otherwise =
|
|
"role " <> role <<> " does not have permission to select computed field" <>> computedField
|
|
|
|
valueParserWithCollectableType
|
|
:: forall pgKind m
|
|
. (Backend ('Postgres pgKind), MonadError QErr m)
|
|
=> (ColumnType ('Postgres pgKind) -> Value -> m S.SQLExp)
|
|
-> CollectableType (ColumnType ('Postgres pgKind))
|
|
-> Value
|
|
-> m S.SQLExp
|
|
valueParserWithCollectableType valBldr pgType val = case pgType of
|
|
CollectableTypeScalar ty -> valBldr ty val
|
|
CollectableTypeArray ofTy -> do
|
|
-- for arrays, we don't use the prepared builder
|
|
vals <- runAesonParser parseJSON val
|
|
scalarValues <- parseScalarValuesColumnType ofTy vals
|
|
return $ S.SETyAnn
|
|
(S.SEArray $ map (toTxtValue . ColumnValue ofTy) scalarValues)
|
|
(S.mkTypeAnn $ CollectableTypeArray (unsafePGColumnToBackend ofTy))
|
|
|
|
binRHSBuilder
|
|
:: forall pgKind m
|
|
. (Backend ('Postgres pgKind), QErrM m)
|
|
=> ColumnType ('Postgres pgKind)
|
|
-> Value
|
|
-> DMLP1T m S.SQLExp
|
|
binRHSBuilder colType val = do
|
|
preparedArgs <- get
|
|
scalarValue <- parseScalarValueColumnType colType val
|
|
put (preparedArgs DS.|> binEncoder scalarValue)
|
|
return $ toPrepParam (DS.length preparedArgs + 1) (unsafePGColumnToBackend colType)
|
|
|
|
fetchRelTabInfo
|
|
:: (QErrM m, TableInfoRM b m, Backend b)
|
|
=> TableName b -> m (TableInfo b)
|
|
fetchRelTabInfo refTabName =
|
|
-- Internal error
|
|
modifyErrAndSet500 ("foreign " <> ) $
|
|
askTabInfoSource refTabName
|
|
|
|
data SessionVariableBuilder b m
|
|
= SessionVariableBuilder
|
|
{ _svbCurrentSession :: !(SQLExpression b)
|
|
, _svbVariableParser :: !(SessionVarType b -> SessionVariable -> m (SQLExpression b))
|
|
}
|
|
|
|
fetchRelDet
|
|
:: (UserInfoM m, QErrM m, TableInfoRM b m, Backend b)
|
|
=> RelName -> TableName b
|
|
-> m (FieldInfoMap (FieldInfo b), SelPermInfo b)
|
|
fetchRelDet relName refTabName = do
|
|
roleName <- askCurRole
|
|
-- Internal error
|
|
refTabInfo <- fetchRelTabInfo refTabName
|
|
-- Get the correct constraint that applies to the given relationship
|
|
refSelPerm <- modifyErr (relPermErr refTabName roleName) $
|
|
askSelPermInfo refTabInfo
|
|
|
|
return (_tciFieldInfoMap $ _tiCoreInfo refTabInfo, refSelPerm)
|
|
where
|
|
relPermErr rTable roleName _ =
|
|
mconcat
|
|
[ "role " <>> roleName
|
|
, " does not have permission to read relationship " <>> relName
|
|
, "; no permission on"
|
|
, " table " <>> rTable
|
|
]
|
|
|
|
checkOnColExp
|
|
:: (UserInfoM m, QErrM m, TableInfoRM b m, Backend b)
|
|
=> SelPermInfo b
|
|
-> SessionVariableBuilder b m
|
|
-> AnnBoolExpFldSQL b
|
|
-> m (AnnBoolExpFldSQL b)
|
|
checkOnColExp spi sessVarBldr annFld = case annFld of
|
|
AVColumn colInfo _ -> do
|
|
let cn = pgiColumn colInfo
|
|
checkSelOnCol spi cn
|
|
return annFld
|
|
AVRelationship relInfo nesAnn -> do
|
|
relSPI <- snd <$> fetchRelDet (riName relInfo) (riRTable relInfo)
|
|
modAnn <- checkSelPerm relSPI sessVarBldr nesAnn
|
|
resolvedFltr <- convAnnBoolExpPartialSQL sessVarBldr $ spiFilter relSPI
|
|
return $ AVRelationship relInfo $ andAnnBoolExps modAnn resolvedFltr
|
|
AVComputedField cfBoolExp -> do
|
|
roleName <- askCurRole
|
|
let fieldName = _acfbName cfBoolExp
|
|
case _acfbBoolExp cfBoolExp of
|
|
CFBEScalar _ -> do
|
|
checkSelectPermOnScalarComputedField spi fieldName
|
|
pure annFld
|
|
CFBETable table nesBoolExp -> do
|
|
tableInfo <- modifyErrAndSet500 ("function " <>) $ askTabInfoSource table
|
|
let errMsg _ = "role " <> roleName <<> " does not have permission to read "
|
|
<> " computed field " <> fieldName <<> "; no permission on table " <>> table
|
|
tableSPI <- modifyErr errMsg $ askSelPermInfo tableInfo
|
|
modBoolExp <- checkSelPerm tableSPI sessVarBldr nesBoolExp
|
|
resolvedFltr <- convAnnBoolExpPartialSQL sessVarBldr $ spiFilter tableSPI
|
|
-- Including table permission filter; "input condition" AND "permission filter condition"
|
|
let finalBoolExp = andAnnBoolExps modBoolExp resolvedFltr
|
|
pure $ AVComputedField cfBoolExp{_acfbBoolExp = CFBETable table finalBoolExp}
|
|
|
|
convAnnBoolExpPartialSQL
|
|
:: (Applicative f, Backend backend)
|
|
=> SessionVariableBuilder backend f
|
|
-> AnnBoolExpPartialSQL backend
|
|
-> f (AnnBoolExpSQL backend)
|
|
convAnnBoolExpPartialSQL f =
|
|
(traverse . traverse) (convPartialSQLExp f)
|
|
|
|
convAnnColumnCaseBoolExpPartialSQL
|
|
:: (Applicative f, Backend backend)
|
|
=> SessionVariableBuilder backend f
|
|
-> AnnColumnCaseBoolExpPartialSQL backend
|
|
-> f (AnnColumnCaseBoolExp backend (SQLExpression backend))
|
|
convAnnColumnCaseBoolExpPartialSQL f =
|
|
(traverse . traverse) (convPartialSQLExp f)
|
|
|
|
convPartialSQLExp
|
|
:: (Applicative f)
|
|
=> SessionVariableBuilder backend f
|
|
-> PartialSQLExp backend
|
|
-> f (SQLExpression backend)
|
|
convPartialSQLExp sessVarBldr = \case
|
|
PSESQLExp sqlExp -> pure sqlExp
|
|
PSESession -> pure $ _svbCurrentSession sessVarBldr
|
|
PSESessVar colTy sessionVariable -> (_svbVariableParser sessVarBldr) colTy sessionVariable
|
|
|
|
sessVarFromCurrentSetting
|
|
:: (Applicative f) => SessionVariableBuilder ('Postgres pgKind) f
|
|
sessVarFromCurrentSetting =
|
|
SessionVariableBuilder currentSession $ \ty var -> pure $ sessVarFromCurrentSetting' ty var
|
|
|
|
sessVarFromCurrentSetting' :: CollectableType PGScalarType -> SessionVariable -> S.SQLExp
|
|
sessVarFromCurrentSetting' ty sessVar =
|
|
withTypeAnn ty $ fromCurrentSession currentSession sessVar
|
|
|
|
withTypeAnn :: CollectableType PGScalarType -> S.SQLExp -> S.SQLExp
|
|
withTypeAnn ty sessVarVal = flip S.SETyAnn (S.mkTypeAnn ty) $
|
|
case ty of
|
|
CollectableTypeScalar baseTy -> withConstructorFn baseTy sessVarVal
|
|
CollectableTypeArray _ -> sessVarVal
|
|
|
|
fromCurrentSession
|
|
:: S.SQLExp
|
|
-> SessionVariable
|
|
-> S.SQLExp
|
|
fromCurrentSession currentSessionExp sessVar =
|
|
S.SEOpApp (S.SQLOp "->>")
|
|
[currentSessionExp, S.SELit $ sessionVariableToText sessVar]
|
|
|
|
currentSession :: S.SQLExp
|
|
currentSession = S.SEUnsafe "current_setting('hasura.user')::json"
|
|
|
|
checkSelPerm
|
|
:: (UserInfoM m, QErrM m, TableInfoRM b m, Backend b)
|
|
=> SelPermInfo b
|
|
-> SessionVariableBuilder b m
|
|
-> AnnBoolExpSQL b
|
|
-> m (AnnBoolExpSQL b)
|
|
checkSelPerm spi sessVarBldr =
|
|
traverse (checkOnColExp spi sessVarBldr)
|
|
|
|
convBoolExp
|
|
:: (UserInfoM m, QErrM m, TableInfoRM b m, BackendMetadata b)
|
|
=> FieldInfoMap (FieldInfo b)
|
|
-> SelPermInfo b
|
|
-> BoolExp b
|
|
-> SessionVariableBuilder b m
|
|
-> TableName b
|
|
-> ValueParser b m (SQLExpression b)
|
|
-> m (AnnBoolExpSQL b)
|
|
convBoolExp cim spi be sessVarBldr rootTable rhsParser = do
|
|
let boolExpRHSParser = BoolExpRHSParser rhsParser $ _svbCurrentSession sessVarBldr
|
|
abe <- annBoolExp boolExpRHSParser rootTable cim $ unBoolExp be
|
|
checkSelPerm spi sessVarBldr abe
|
|
|
|
dmlTxErrorHandler :: Q.PGTxErr -> QErr
|
|
dmlTxErrorHandler = mkTxErrorHandler $ \case
|
|
PGIntegrityConstraintViolation _ -> True
|
|
PGDataException _ -> True
|
|
PGSyntaxErrorOrAccessRuleViolation (Just (PGErrorSpecific code)) -> code `elem`
|
|
[ PGUndefinedObject
|
|
, PGInvalidColumnReference ]
|
|
_ -> False
|
|
|
|
-- validate headers
|
|
validateHeaders :: (UserInfoM m, QErrM m) => HashSet Text -> m ()
|
|
validateHeaders depHeaders = do
|
|
headers <- getSessionVariables . _uiSession <$> askUserInfo
|
|
forM_ depHeaders $ \hdr ->
|
|
unless (hdr `elem` map T.toLower headers) $
|
|
throw400 NotFound $ hdr <<> " header is expected but not found"
|
|
|
|
-- validate limit and offset int values
|
|
onlyPositiveInt :: MonadError QErr m => Int -> m ()
|
|
onlyPositiveInt i = when (i < 0) $ throw400 NotSupported
|
|
"unexpected negative value"
|