graphql-engine/server/src-lib/Hasura/RQL/DDL/Permission/Internal.hs

297 lines
8.7 KiB
Haskell

{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeFamilyDependencies #-}
module Hasura.RQL.DDL.Permission.Internal where
import Hasura.Prelude
import qualified Data.HashMap.Strict as M
import qualified Data.Text as T
import qualified Database.PG.Query as Q
import qualified Hasura.Backends.Postgres.SQL.DML as S
import Control.Lens hiding ((.=))
import Data.Aeson.Casing
import Data.Aeson.TH
import Data.Aeson.Types
import Data.Text.Extended
import Instances.TH.Lift ()
import Language.Haskell.TH.Syntax (Lift)
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Backends.Postgres.SQL.Value
import Hasura.EncJSON
import Hasura.Incremental (Cacheable)
import Hasura.RQL.GBoolExp
import Hasura.RQL.Types
import Hasura.Server.Utils
import Hasura.Session
data PermColSpec
= PCStar
| PCCols ![PGCol]
deriving (Show, Eq, Lift, Generic)
instance Cacheable PermColSpec
instance FromJSON PermColSpec where
parseJSON (String "*") = return PCStar
parseJSON x = PCCols <$> parseJSON x
instance ToJSON PermColSpec where
toJSON (PCCols cols) = toJSON cols
toJSON PCStar = "*"
convColSpec :: FieldInfoMap (FieldInfo 'Postgres) -> PermColSpec -> [PGCol]
convColSpec _ (PCCols cols) = cols
convColSpec cim PCStar = map pgiColumn $ getCols cim
permissionIsDefined
:: Maybe (RolePermInfo backend) -> PermAccessor backend a -> Bool
permissionIsDefined rpi pa =
isJust $ join $ rpi ^? _Just.permAccToLens pa
assertPermDefined
:: (MonadError QErr m)
=> RoleName
-> PermAccessor backend a
-> TableInfo backend
-> m ()
assertPermDefined roleName pa tableInfo =
unless (permissionIsDefined rpi pa) $ throw400 PermissionDenied $ mconcat
[ "'" <> T.pack (show $ permAccToType pa) <> "'"
, " permission on " <>> _tciName (_tiCoreInfo tableInfo)
, " for role " <>> roleName
, " does not exist"
]
where
rpi = M.lookup roleName $ _tiRolePermInfoMap tableInfo
askPermInfo
:: (MonadError QErr m)
=> TableInfo backend
-> RoleName
-> PermAccessor backend c
-> m c
askPermInfo tabInfo roleName pa =
case M.lookup roleName rpim >>= (^. paL) of
Just c -> return c
Nothing -> throw400 PermissionDenied $ mconcat
[ pt <> " permission on " <>> _tciName (_tiCoreInfo tabInfo)
, " for role " <>> roleName
, " does not exist"
]
where
paL = permAccToLens pa
pt = permTypeToCode $ permAccToType pa
rpim = _tiRolePermInfoMap tabInfo
savePermToCatalog
:: (ToJSON a)
=> PermType
-> QualifiedTable
-> PermDef a
-> SystemDefined
-> Q.TxE QErr ()
savePermToCatalog pt (QualifiedObject sn tn) (PermDef rn qdef mComment) systemDefined =
Q.unitQE defaultTxErrorHandler [Q.sql|
INSERT INTO
hdb_catalog.hdb_permission
(table_schema, table_name, role_name, perm_type, perm_def, comment, is_system_defined)
VALUES ($1, $2, $3, $4, $5 :: jsonb, $6, $7)
|] (sn, tn, rn, permTypeToCode pt, Q.AltJ qdef, mComment, systemDefined) True
updatePermDefInCatalog
:: (ToJSON a)
=> PermType
-> QualifiedTable
-> RoleName
-> a
-> Q.TxE QErr ()
updatePermDefInCatalog pt (QualifiedObject sn tn) rn qdef =
Q.unitQE defaultTxErrorHandler [Q.sql|
UPDATE hdb_catalog.hdb_permission
SET perm_def = $1 :: jsonb
WHERE table_schema = $2 AND table_name = $3
AND role_name = $4 AND perm_type = $5
|] (Q.AltJ qdef, sn, tn, rn, permTypeToCode pt) True
dropPermFromCatalog
:: QualifiedTable
-> RoleName
-> PermType
-> Q.TxE QErr ()
dropPermFromCatalog (QualifiedObject sn tn) rn pt =
Q.unitQE defaultTxErrorHandler [Q.sql|
DELETE FROM
hdb_catalog.hdb_permission
WHERE
table_schema = $1
AND table_name = $2
AND role_name = $3
AND perm_type = $4
|] (sn, tn, rn, permTypeToCode pt) True
type CreatePerm a = WithTable (PermDef a)
data PermDef a =
PermDef
{ pdRole :: !RoleName
, pdPermission :: !a
, pdComment :: !(Maybe Text)
} deriving (Show, Eq, Lift, Generic)
instance (Cacheable a) => Cacheable (PermDef a)
$(deriveFromJSON (aesonDrop 2 snakeCase){omitNothingFields=True} ''PermDef)
instance (ToJSON a) => ToJSON (PermDef a) where
toJSON = object . toAesonPairs
instance (ToJSON a) => ToAesonPairs (PermDef a) where
toAesonPairs (PermDef rn perm comment) =
[ "role" .= rn
, "permission" .= perm
, "comment" .= comment
]
data CreatePermP1Res a
= CreatePermP1Res
{ cprInfo :: !a
, cprDeps :: ![SchemaDependency]
} deriving (Show, Eq)
procBoolExp
:: (QErrM m, TableCoreInfoRM m)
=> QualifiedTable
-> FieldInfoMap (FieldInfo 'Postgres)
-> BoolExp
-> m (AnnBoolExpPartialSQL 'Postgres, [SchemaDependency])
procBoolExp tn fieldInfoMap be = do
abe <- annBoolExp valueParser fieldInfoMap $ unBoolExp be
let deps = getBoolExpDeps tn abe
return (abe, deps)
isReqUserId :: Text -> Bool
isReqUserId = (== "req_user_id") . T.toLower
getDepHeadersFromVal :: Value -> [Text]
getDepHeadersFromVal val = case val of
Object o -> parseObject o
_ -> parseOnlyString val
where
parseOnlyString v = case v of
(String t)
| isSessionVariable t -> [T.toLower t]
| isReqUserId t -> [userIdHeader]
| otherwise -> []
_ -> []
parseObject o =
concatMap getDepHeadersFromVal (M.elems o)
getDependentHeaders :: BoolExp -> [Text]
getDependentHeaders (BoolExp boolExp) =
flip foldMap boolExp $ \(ColExp _ v) -> getDepHeadersFromVal v
valueParser
:: (MonadError QErr m)
=> PGType (ColumnType 'Postgres) -> Value -> m (PartialSQLExp 'Postgres)
valueParser pgType = \case
-- When it is a special variable
String t
| isSessionVariable t -> return $ mkTypedSessionVar pgType $ mkSessionVariable t
| isReqUserId t -> return $ mkTypedSessionVar pgType userIdHeader
-- Typical value as Aeson's value
val -> case pgType of
PGTypeScalar columnType -> PSESQLExp . toTxtValue <$> parsePGScalarValue columnType val
PGTypeArray ofType -> do
vals <- runAesonParser parseJSON val
WithScalarType scalarType scalarValues <- parsePGScalarValues ofType vals
return . PSESQLExp $ S.SETyAnn
(S.SEArray $ map (toTxtValue . WithScalarType scalarType) scalarValues)
(S.mkTypeAnn $ PGTypeArray scalarType)
injectDefaults :: QualifiedTable -> QualifiedTable -> Q.Query
injectDefaults qv qt =
Q.fromText $ mconcat
[ "SELECT hdb_catalog.inject_table_defaults("
, pgFmtLit vsn
, ", "
, pgFmtLit vn
, ", "
, pgFmtLit tsn
, ", "
, pgFmtLit tn
, ");"
]
where
QualifiedObject (SchemaName vsn) (TableName vn) = qv
QualifiedObject (SchemaName tsn) (TableName tn) = qt
data DropPerm a
= DropPerm
{ dipTable :: !QualifiedTable
, dipRole :: !RoleName
} deriving (Show, Eq, Lift)
$(deriveJSON (aesonDrop 3 snakeCase){omitNothingFields=True} ''DropPerm)
type family PermInfo a = r | r -> a
class (ToJSON a) => IsPerm a where
permAccessor
:: PermAccessor 'Postgres (PermInfo a)
buildPermInfo
:: (QErrM m, TableCoreInfoRM m)
=> QualifiedTable
-> FieldInfoMap (FieldInfo 'Postgres)
-> PermDef a
-> m (WithDeps (PermInfo a))
getPermAcc1
:: PermDef a -> PermAccessor 'Postgres (PermInfo a)
getPermAcc1 _ = permAccessor
getPermAcc2
:: DropPerm a -> PermAccessor 'Postgres (PermInfo a)
getPermAcc2 _ = permAccessor
addPermP2 :: (IsPerm a, MonadTx m, HasSystemDefined m) => QualifiedTable -> PermDef a -> m ()
addPermP2 tn pd = do
let pt = permAccToType $ getPermAcc1 pd
systemDefined <- askSystemDefined
liftTx $ savePermToCatalog pt tn pd systemDefined
runCreatePerm
:: (UserInfoM m, CacheRWM m, IsPerm a, MonadTx m, HasSystemDefined m)
=> CreatePerm a -> m EncJSON
runCreatePerm (WithTable tn pd) = do
addPermP2 tn pd
let pt = permAccToType $ getPermAcc1 pd
buildSchemaCacheFor $ MOTableObj tn (MTOPerm (pdRole pd) pt)
pure successMsg
dropPermP1
:: (QErrM m, CacheRM m, IsPerm a)
=> DropPerm a -> m (PermInfo a)
dropPermP1 dp@(DropPerm tn rn) = do
tabInfo <- askTabInfo tn
askPermInfo tabInfo rn $ getPermAcc2 dp
dropPermP2 :: forall a m. (MonadTx m, IsPerm a) => DropPerm a -> m ()
dropPermP2 dp@(DropPerm tn rn) =
liftTx $ dropPermFromCatalog tn rn pt
where
pa = getPermAcc2 dp
pt = permAccToType pa
runDropPerm
:: (IsPerm a, UserInfoM m, CacheRWM m, MonadTx m)
=> DropPerm a -> m EncJSON
runDropPerm defn = do
dropPermP1 defn
dropPermP2 defn
withNewInconsistentObjsCheck buildSchemaCache
return successMsg