mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 13:02:11 +03:00
6bddaa7564
https://github.com/hasura/graphql-engine-mono/pull/1539 GitOrigin-RevId: 7444973e9cc7e0598eeb1bd7951ad45dd37ec550
332 lines
15 KiB
Haskell
332 lines
15 KiB
Haskell
{-# LANGUAGE Arrows #-}
|
|
{-# LANGUAGE ViewPatterns #-}
|
|
|
|
module Hasura.RQL.DDL.Schema.Cache.Permission
|
|
( buildTablePermissions
|
|
, mkPermissionMetadataObject
|
|
, mkRemoteSchemaPermissionMetadataObject
|
|
, orderRoles
|
|
) where
|
|
|
|
import Hasura.Prelude
|
|
|
|
import qualified Data.Graph as G
|
|
import qualified Data.HashMap.Strict as M
|
|
import qualified Data.HashMap.Strict.Extended as M
|
|
import qualified Data.HashSet as Set
|
|
import qualified Data.List.NonEmpty as NE
|
|
import qualified Data.Sequence as Seq
|
|
|
|
import Control.Arrow.Extended
|
|
import Data.Aeson
|
|
import Data.Proxy
|
|
import Data.Text.Extended
|
|
|
|
import qualified Hasura.Incremental as Inc
|
|
import qualified Hasura.SQL.AnyBackend as AB
|
|
|
|
import Hasura.Base.Error
|
|
import Hasura.RQL.DDL.Permission
|
|
import Hasura.RQL.DDL.Schema.Cache.Common
|
|
import Hasura.RQL.Types
|
|
import Hasura.Server.Types
|
|
import Hasura.Session
|
|
|
|
|
|
{- Note: [Inherited roles architecture for postgres read queries]
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
1. Schema generation
|
|
--------------------
|
|
|
|
Schema generation for inherited roles is similar to the schema
|
|
generation of non-inherited roles. In the case of inherited roles,
|
|
we combine the `SelectPermInfo`s (see `combineSelectPermInfos`) of the
|
|
inherited role's role set and a new `SelectPermInfo` will be generated
|
|
which will be the select permission of the inherited role.
|
|
|
|
2. SQL generation
|
|
-----------------
|
|
|
|
See note [SQL generation for inherited roles]
|
|
|
|
3. Introspection
|
|
----------------
|
|
|
|
The columns accessible to an inherited role are explicitly set to
|
|
nullable irrespective of the nullability of the DB column to accomodate
|
|
cell value nullification.
|
|
|
|
-}
|
|
|
|
-- | This type is only used in the `combineSelectPermInfos` for
|
|
-- combining select permissions efficiently
|
|
data CombinedSelPermInfo (b :: BackendType)
|
|
= CombinedSelPermInfo
|
|
{ cspiCols :: ![(M.HashMap (Column b) (Maybe (AnnColumnCaseBoolExpPartialSQL b)))]
|
|
, cspiScalarComputedFields :: ![(M.HashMap ComputedFieldName (Maybe (AnnColumnCaseBoolExpPartialSQL b)))]
|
|
, cspiFilter :: ![(AnnBoolExpPartialSQL b)]
|
|
, cspiLimit :: !(Maybe Int)
|
|
, cspiAllowAgg :: !Bool
|
|
, cspiRequiredHeaders :: !(Set.HashSet Text)
|
|
}
|
|
|
|
|
|
-- | combineSelectPermInfos combines multiple `SelPermInfo`s
|
|
-- into one `SelPermInfo`. Two `SelPermInfo` will
|
|
-- be combined in the following manner:
|
|
--
|
|
-- 1. Columns - The `SelPermInfo` contains a hashset of the columns that are
|
|
-- accessible to the role. To combine two `SelPermInfo`s, every column of the
|
|
-- hashset is coupled with the boolean expression (filter) of the `SelPermInfo`
|
|
-- and a hash map of all the columns is created out of it, this hashmap is
|
|
-- generated for the `SelPermInfo`s that are going to be combined. These hashmaps
|
|
-- are then unioned and the values of these hashmaps are `OR`ed. When a column
|
|
-- is accessible to all the select permissions then the nullability of the column
|
|
-- is inferred from the DB column otherwise the column is explicitly marked as
|
|
-- nullable to accomodate cell-value nullification.
|
|
-- 2. Scalar computed fields - Scalar computed fields work the same as Columns (#1)
|
|
-- 3. Filter / Boolean expression - The filters are combined using a `BoolOr`
|
|
-- 4. Limit - Limits are combined by taking the maximum of the two limits
|
|
-- 5. Allow Aggregation - Aggregation is allowed, if any of the permissions allow it.
|
|
-- 6. Request Headers - Request headers are concatenated
|
|
--
|
|
-- To maintain backwards compatibility, we handle the case of single select permission
|
|
-- differently i.e. we don't want the case statements that always evaluate to true with
|
|
-- the columns
|
|
--
|
|
combineSelectPermInfos
|
|
:: forall b
|
|
. (Backend b)
|
|
=> NE.NonEmpty (SelPermInfo b)
|
|
-> SelPermInfo b
|
|
combineSelectPermInfos (headSelPermInfo NE.:| []) = headSelPermInfo
|
|
combineSelectPermInfos selPermInfos@(headSelPermInfo NE.:| restSelPermInfos) =
|
|
let CombinedSelPermInfo {..}
|
|
= foldr combine (modifySingleSelectPerm headSelPermInfo) restSelPermInfos
|
|
mergeColumnsWithBoolExp xs
|
|
| length selPermInfos == length xs = Nothing
|
|
| otherwise = foldr combineCaseBoolExps Nothing xs
|
|
in SelPermInfo (mergeColumnsWithBoolExp <$> M.unionsAll cspiCols)
|
|
(mergeColumnsWithBoolExp <$> M.unionsAll cspiScalarComputedFields)
|
|
(BoolOr cspiFilter)
|
|
cspiLimit
|
|
cspiAllowAgg
|
|
(toList cspiRequiredHeaders)
|
|
where
|
|
modifySingleSelectPerm :: SelPermInfo b -> CombinedSelPermInfo b
|
|
modifySingleSelectPerm SelPermInfo {..} =
|
|
let columnCaseBoolExp = fmap AnnColumnCaseBoolExpField spiFilter
|
|
colsWithColCaseBoolExp = spiCols $> Just columnCaseBoolExp
|
|
scalarCompFieldsWithColCaseBoolExp = spiScalarComputedFields $> Just columnCaseBoolExp
|
|
in
|
|
CombinedSelPermInfo [colsWithColCaseBoolExp]
|
|
[scalarCompFieldsWithColCaseBoolExp]
|
|
[spiFilter]
|
|
spiLimit
|
|
spiAllowAgg
|
|
(Set.fromList spiRequiredHeaders)
|
|
|
|
combine :: SelPermInfo b -> CombinedSelPermInfo b -> CombinedSelPermInfo b
|
|
combine (modifySingleSelectPerm -> lSelPermInfo) accSelPermInfo =
|
|
CombinedSelPermInfo
|
|
{ cspiCols = (cspiCols lSelPermInfo) <> (cspiCols accSelPermInfo)
|
|
, cspiScalarComputedFields =
|
|
(cspiScalarComputedFields lSelPermInfo) <> (cspiScalarComputedFields accSelPermInfo)
|
|
, cspiFilter = (cspiFilter lSelPermInfo) <> (cspiFilter accSelPermInfo)
|
|
, cspiLimit =
|
|
case (cspiLimit lSelPermInfo, cspiLimit accSelPermInfo) of
|
|
(Nothing, Nothing) -> Nothing
|
|
(Just l, Nothing) -> Just l
|
|
(Nothing, Just r) -> Just r
|
|
(Just l , Just r) -> Just $ max l r
|
|
, cspiAllowAgg = cspiAllowAgg lSelPermInfo || cspiAllowAgg accSelPermInfo
|
|
, cspiRequiredHeaders = (cspiRequiredHeaders lSelPermInfo) <> (cspiRequiredHeaders accSelPermInfo)
|
|
}
|
|
|
|
combineCaseBoolExps l r =
|
|
case (l, r) of
|
|
(Nothing, Nothing) -> Nothing
|
|
(Just caseBoolExp, Nothing) -> Just caseBoolExp
|
|
(Nothing, Just caseBoolExp) -> Just caseBoolExp
|
|
(Just caseBoolExpL, Just caseBoolExpR) -> Just $ BoolOr [caseBoolExpL, caseBoolExpR]
|
|
|
|
-- | 'orderRoles' is used to order the roles, in such a way that given
|
|
-- a role R with n parent roles - PR1, PR2 .. PRn, then the 'orderRoles'
|
|
-- function will order the roles in such a way that all the parent roles
|
|
-- precede the role R. Note that the order of the parent roles itself doesn't
|
|
-- matter as long as they precede the roles on which they are dependent on.
|
|
--
|
|
-- For example, the orderRoles may return `[PR1, PR3, PR2, ... PRn, R]`
|
|
-- or `[PR5, PR3, PR1 ... R]`, both of them are correct because all
|
|
-- the parent roles precede the inherited role R, assuming the parent roles
|
|
-- themselves don't have any parents for the sake of this example.
|
|
orderRoles
|
|
:: MonadError QErr m
|
|
=> [Role]
|
|
-> m [Role]
|
|
orderRoles allRoles = do
|
|
-- inherited roles can be created from other inherited and non-inherited roles
|
|
-- So, roles can be thought of as a graph where non-inherited roles don't have
|
|
-- any outgoing edges and inherited roles as nodes with edges to its parent roles
|
|
-- However, we can't allow cyclic roles since permissions built by a role is used
|
|
-- by the dependent roles to build their permissions and if cyclic roles were to be
|
|
-- allowed, the permissions building will be stuck in an infinite loop
|
|
let graphNodesList = [(role, _rRoleName role, toList (_unParentRoles . _rParentRoles $ role)) | role <- allRoles]
|
|
let orderedGraphNodes = G.stronglyConnComp graphNodesList -- topologically sort the nodes of the graph
|
|
cyclicRoles = filter checkCycle orderedGraphNodes
|
|
unless (null cyclicRoles) $ do
|
|
-- we're appending the first element of the list at the end, so that the error message will
|
|
-- contain the complete cycle of the roles
|
|
let roleCycles = map (tshow . map (roleNameToTxt . _rRoleName) . appendFirstElementAtEnd . G.flattenSCC) cyclicRoles
|
|
throw400 CyclicDependency $ "found cycle(s) in roles: " <> commaSeparated roleCycles
|
|
let allOrderedRoles = G.flattenSCCs orderedGraphNodes
|
|
pure allOrderedRoles
|
|
where
|
|
checkCycle = \case
|
|
G.AcyclicSCC _ -> False
|
|
G.CyclicSCC _ -> True
|
|
|
|
appendFirstElementAtEnd [] = []
|
|
appendFirstElementAtEnd (x:xs) = (x:xs) ++ [x]
|
|
|
|
buildTablePermissions
|
|
:: forall b arr m
|
|
. ( ArrowChoice arr, Inc.ArrowDistribute arr, Inc.ArrowCache m arr
|
|
, MonadError QErr m, ArrowWriter (Seq CollectedInfo) arr
|
|
, HasServerConfigCtx m
|
|
, BackendMetadata b
|
|
, Inc.Cacheable (Proxy b)
|
|
)
|
|
=> ( Proxy b
|
|
, SourceName
|
|
, Inc.Dependency (TableCoreCache b)
|
|
, FieldInfoMap (FieldInfo b)
|
|
, TablePermissionInputs b
|
|
, [Role]
|
|
) `arr` (RolePermInfoMap b)
|
|
buildTablePermissions = Inc.cache proc (proxy, source, tableCache, tableFields, tablePermissions, orderedRoles) -> do
|
|
let alignedPermissions = alignPermissions tablePermissions
|
|
table = _tpiTable tablePermissions
|
|
experimentalFeatures <- bindA -< _sccExperimentalFeatures <$> askServerConfigCtx
|
|
metadataRolePermissions <-
|
|
(| Inc.keyed (\_ (insertPermission, selectPermission, updatePermission, deletePermission) -> do
|
|
insert <- buildPermission -< (proxy, tableCache, source, table, tableFields, listToMaybe insertPermission)
|
|
select <- buildPermission -< (proxy, tableCache, source, table, tableFields, listToMaybe selectPermission)
|
|
update <- buildPermission -< (proxy, tableCache, source, table, tableFields, listToMaybe updatePermission)
|
|
delete <- buildPermission -< (proxy, tableCache, source, table, tableFields, listToMaybe deletePermission)
|
|
returnA -< RolePermInfo insert select update delete)
|
|
|) alignedPermissions
|
|
-- see [Inherited roles architecture for postgres read queries]
|
|
(| foldlA' (\accumulatedRolePermMap (Role roleName (ParentRoles parentRoles)) -> do
|
|
let metadataRoleSelectPermission = _permSel =<< M.lookup roleName accumulatedRolePermMap
|
|
roleSelectPermission <-
|
|
bindA -<
|
|
case metadataRoleSelectPermission of
|
|
-- There's an permission set for the role in the metadata, so we use that
|
|
Just metadataRoleSelectPermission' -> pure $ Just metadataRoleSelectPermission'
|
|
-- no permission found in the metadata, so we inherit permissions from the parent
|
|
-- roles
|
|
Nothing -> do
|
|
parentRoleTablePerms <-
|
|
for (toList parentRoles) $ \role ->
|
|
onNothing (M.lookup role accumulatedRolePermMap) $ throw500 $
|
|
-- this error will ideally never be thrown, but if it's thrown then
|
|
-- it's possible that the permissions for the role do exist, but it's
|
|
-- not yet built due to wrong ordering of the roles, check `orderRoles`
|
|
"buildTablePermissions: table role permissions for role: " <> role <<> " not found"
|
|
pure $ combineSelectPermInfos <$> NE.nonEmpty (catMaybes $ _permSel <$> parentRoleTablePerms)
|
|
let rolePermInfo = RolePermInfo Nothing roleSelectPermission Nothing Nothing
|
|
returnA -< M.insert roleName rolePermInfo accumulatedRolePermMap)
|
|
|) metadataRolePermissions (bool mempty orderedRoles $ EFInheritedRoles `elem` experimentalFeatures)
|
|
-- build permissions for inherited roles only when inherited roles is enabled
|
|
where
|
|
mkMap :: [PermDef a] -> HashMap RoleName (PermDef a)
|
|
mkMap = mapFromL _pdRole
|
|
|
|
alignPermissions TablePermissionInputs{..} =
|
|
let insertsMap = M.map (\a -> ([a], [], [], [])) (mkMap _tpiInsert)
|
|
selectsMap = M.map (\a -> ([], [a], [], [])) (mkMap _tpiSelect)
|
|
updatesMap = M.map (\a -> ([], [], [a], [])) (mkMap _tpiUpdate)
|
|
deletesMap = M.map (\a -> ([], [], [], [a])) (mkMap _tpiDelete)
|
|
unionMap = M.unionWith (<>)
|
|
in insertsMap `unionMap` selectsMap `unionMap` updatesMap `unionMap` deletesMap
|
|
|
|
mkPermissionMetadataObject
|
|
:: forall b a
|
|
. (ToJSON (a b), BackendMetadata b, IsPerm a)
|
|
=> SourceName
|
|
-> TableName b
|
|
-> PermDef (a b)
|
|
-> MetadataObject
|
|
mkPermissionMetadataObject source table permDef =
|
|
let permType = permAccToType (permAccessor :: PermAccessor b (PermInfo a b))
|
|
objectId = MOSourceObjId source
|
|
$ AB.mkAnyBackend
|
|
$ SMOTableObj @b table
|
|
$ MTOPerm (_pdRole permDef) permType
|
|
definition = toJSON $ WithTable @b source table permDef
|
|
in MetadataObject objectId definition
|
|
|
|
mkRemoteSchemaPermissionMetadataObject
|
|
:: AddRemoteSchemaPermissions
|
|
-> MetadataObject
|
|
mkRemoteSchemaPermissionMetadataObject (AddRemoteSchemaPermissions rsName roleName defn _) =
|
|
let objectId = MORemoteSchemaPermissions rsName roleName
|
|
in MetadataObject objectId $ toJSON defn
|
|
|
|
withPermission
|
|
:: forall bknd a b c s arr
|
|
. (ArrowChoice arr, ArrowWriter (Seq CollectedInfo) arr, BackendMetadata bknd, ToJSON (c bknd), IsPerm c)
|
|
=> WriterA (Seq SchemaDependency) (ErrorA QErr arr) (a, s) b
|
|
-> ( a
|
|
, ((SourceName, TableName bknd, PermDef (c bknd), Proxy bknd), s)
|
|
) `arr` (Maybe b)
|
|
withPermission f = proc (e, ((source, table, permission, _proxy), s)) -> do
|
|
let metadataObject = mkPermissionMetadataObject @bknd source table permission
|
|
permType = permAccToType (permAccessor :: PermAccessor bknd (PermInfo c bknd))
|
|
roleName = _pdRole permission
|
|
schemaObject = SOSourceObj source
|
|
$ AB.mkAnyBackend
|
|
$ SOITableObj @bknd table
|
|
$ TOPerm roleName permType
|
|
addPermContext err = "in permission for role " <> roleName <<> ": " <> err
|
|
(| withRecordInconsistency (
|
|
(| withRecordDependencies (
|
|
(| modifyErrA (f -< (e, s))
|
|
|) (addTableContext @bknd table . addPermContext))
|
|
|) metadataObject schemaObject)
|
|
|) metadataObject
|
|
|
|
buildPermission
|
|
:: forall b a arr m
|
|
. ( ArrowChoice arr
|
|
, ArrowWriter (Seq CollectedInfo) arr
|
|
, Inc.ArrowCache m arr
|
|
, Inc.Cacheable (a b)
|
|
, Inc.Cacheable (Proxy b)
|
|
, MonadError QErr m
|
|
, BackendMetadata b
|
|
, ToJSON (a b)
|
|
, IsPerm a
|
|
)
|
|
=> ( Proxy b
|
|
, Inc.Dependency (TableCoreCache b)
|
|
, SourceName
|
|
, TableName b
|
|
, FieldInfoMap (FieldInfo b)
|
|
, Maybe (PermDef (a b))
|
|
) `arr` Maybe (PermInfo a b)
|
|
buildPermission = Inc.cache proc (proxy, tableCache, source, table, tableFields, maybePermission) -> do
|
|
(| traverseA ( \permission ->
|
|
(| withPermission (do
|
|
bindErrorA -< when (_pdRole permission == adminRoleName) $
|
|
throw400 ConstraintViolation "cannot define permission for admin role"
|
|
(info, dependencies) <- liftEitherA <<< Inc.bindDepend -< runExceptT $
|
|
runTableCoreCacheRT (buildPermInfo source table tableFields permission) (source, tableCache)
|
|
tellA -< Seq.fromList dependencies
|
|
returnA -< info)
|
|
|) (source, table, permission, proxy))
|
|
|) maybePermission
|
|
>-> (\info -> join info >- returnA)
|