graphql-engine/server/src-lib/Hasura/RQL/DDL/QueryCollection.hs
Swann Moreau 8bd34b4a51 server, pro: add support for per-role allowlists
spec: https://github.com/hasura/graphql-engine-mono/pull/2278

Briefly:
- extend metadata so that allowlist entries get a new scope field
- update `add_collection_to_allowlist` to accept this new scope field,
  and adds `update_scope_of_collection_in_allowlist` to change the scope
- scope can be global or role-based; a collection is available for every
  role if it is global, and available to every listed role if it is role-based
- graphql-engine-oss is aware of role-based allowlist metadata; collections
  with non-global scope are treated as if they weren't in the allowlist

To run the tests:
- `cabal run graphql-engine-tests -- unit --match Allowlist`
- py-tests against pro:
  - launch `graphql-engine-pro` with `HASURA_GRAPHQL_ADMIN_SECRET` and `HASURA_GRAPHQL_ENABLE_ALLOWLIST`
  - `pytest test_allowlist_queries.py --hge-urls=... --pg-urls=... --hge-key=... --test-allowlist-queries --pro-tests`

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2477
Co-authored-by: Anon Ray <616387+ecthiender@users.noreply.github.com>
Co-authored-by: Robert <132113+robx@users.noreply.github.com>
GitOrigin-RevId: 01f8026fbe59d8701e2de30986511a452fce1a99
2022-02-08 16:54:49 +00:00

204 lines
6.8 KiB
Haskell

module Hasura.RQL.DDL.QueryCollection
( runCreateCollection,
runDropCollection,
runAddQueryToCollection,
runDropQueryFromCollection,
runAddCollectionToAllowlist,
runDropCollectionFromAllowlist,
runUpdateScopeOfCollectionInAllowlist,
)
where
import Control.Lens ((.~))
import Data.Aeson qualified as Aeson
import Data.HashMap.Strict.InsOrd qualified as OMap
import Data.List.Extended (duplicates)
import Data.Text.Extended
import Data.Text.NonEmpty
import Hasura.Base.Error
import Hasura.EncJSON
import Hasura.Prelude
import Hasura.RQL.Types
addCollectionP2 ::
(QErrM m) =>
CollectionDef ->
m ()
addCollectionP2 (CollectionDef queryList) =
withPathK "queries" $
unless (null duplicateNames) $
throw400 NotSupported $
"found duplicate query names "
<> dquoteList (unNonEmptyText . unQueryName <$> toList duplicateNames)
where
duplicateNames = duplicates $ map _lqName queryList
runCreateCollection ::
(QErrM m, CacheRWM m, MetadataM m) =>
CreateCollection ->
m EncJSON
runCreateCollection cc = do
collDetM <- getCollectionDefM collName
withPathK "name" $
onJust collDetM $
const $
throw400 AlreadyExists $
"query collection with name " <> collName <<> " already exists"
withPathK "definition" $ addCollectionP2 def
withNewInconsistentObjsCheck $
buildSchemaCache $
MetadataModifier $
metaQueryCollections %~ OMap.insert collName cc
return successMsg
where
CreateCollection collName def _ = cc
runAddQueryToCollection ::
(CacheRWM m, MonadError QErr m, MetadataM m) =>
AddQueryToCollection ->
m EncJSON
runAddQueryToCollection (AddQueryToCollection collName queryName query) = do
(CreateCollection _ (CollectionDef qList) comment) <- getCollectionDef collName
let queryExists = flip any qList $ \q -> _lqName q == queryName
when queryExists $
throw400 AlreadyExists $
"query with name "
<> queryName <<> " already exists in collection " <>> collName
let collDef = CollectionDef $ qList <> pure listQ
withNewInconsistentObjsCheck $
buildSchemaCache $
MetadataModifier $
metaQueryCollections
%~ OMap.insert collName (CreateCollection collName collDef comment)
return successMsg
where
listQ = ListedQuery queryName query
runDropCollection ::
(MonadError QErr m, MetadataM m, CacheRWM m) =>
DropCollection ->
m EncJSON
runDropCollection (DropCollection collName cascade) = do
cascadeModifier <- withPathK "collection" $ do
assertCollectionDefined collName
allowlist <- fetchAllAllowlistCollections
if (collName `elem` allowlist)
then
if not cascade
then
throw400 DependencyError $
"query collection with name "
<> collName <<> " is present in the allowlist; cannot proceed to drop. "
<> "please use cascade to confirm you wish to drop it from the allowlist as well"
else dropCollectionFromAllowlist collName
else pure mempty
withNewInconsistentObjsCheck $
buildSchemaCache $
cascadeModifier <> MetadataModifier (metaQueryCollections %~ OMap.delete collName)
pure successMsg
runDropQueryFromCollection ::
(CacheRWM m, MonadError QErr m, MetadataM m) =>
DropQueryFromCollection ->
m EncJSON
runDropQueryFromCollection (DropQueryFromCollection collName queryName) = do
CreateCollection _ (CollectionDef qList) _ <- getCollectionDef collName
let queryExists = flip any qList $ \q -> _lqName q == queryName
unless queryExists $
throw400 NotFound $
"query with name "
<> queryName <<> " not found in collection " <>> collName
withNewInconsistentObjsCheck $
buildSchemaCache $
MetadataModifier $
metaQueryCollections . ix collName . ccDefinition . cdQueries
%~ filter ((/=) queryName . _lqName)
pure successMsg
runAddCollectionToAllowlist ::
(MonadError QErr m, MetadataM m, CacheRWM m) =>
AllowlistEntry ->
m EncJSON
runAddCollectionToAllowlist entry = do
withPathK "collection" $ assertCollectionDefined (aeCollection entry)
allowlist <- withPathK "allowlist" fetchAllowlist
case metadataAllowlistInsert entry allowlist of
Left msg ->
pure . encJFromJValue . Aeson.object $
["message" Aeson..= msg]
Right allowlist' -> do
withNewInconsistentObjsCheck . buildSchemaCache $ MetadataModifier (metaAllowlist .~ allowlist')
pure successMsg
-- Create a metadata modifier that drops a collection from the allowlist.
-- This is factored out for use in 'runDropCollection'.
dropCollectionFromAllowlist ::
(MonadError QErr m, MetadataM m) =>
CollectionName ->
m MetadataModifier
dropCollectionFromAllowlist collName = do
withPathK "collection" $ assertCollectionDefined collName
allowList <- withPathK "allowlist" fetchAllowlist
case OMap.lookup collName allowList of
Nothing -> throw400 NotFound $ "collection " <> collName <<> " doesn't exist in the allowlist"
Just _ -> pure $ MetadataModifier $ metaAllowlist .~ OMap.delete collName allowList
runDropCollectionFromAllowlist ::
(MonadError QErr m, MetadataM m, CacheRWM m) =>
DropCollectionFromAllowlist ->
m EncJSON
runDropCollectionFromAllowlist (DropCollectionFromAllowlist collName) = do
withNewInconsistentObjsCheck . buildSchemaCache =<< dropCollectionFromAllowlist collName
return successMsg
runUpdateScopeOfCollectionInAllowlist ::
(MonadError QErr m, MetadataM m, CacheRWM m) =>
UpdateScopeOfCollectionInAllowlist ->
m EncJSON
runUpdateScopeOfCollectionInAllowlist (UpdateScopeOfCollectionInAllowlist entry) = do
withPathK "collection" $ assertCollectionDefined (aeCollection entry)
al <- withPathK "allowlist" fetchAllowlist
modifier <- case metadataAllowlistUpdateScope entry al of
Left err -> throw400 NotFound err
Right al' ->
pure . MetadataModifier $
metaAllowlist .~ al'
withNewInconsistentObjsCheck $ buildSchemaCache modifier
return successMsg
-- helpers
assertCollectionDefined :: (QErrM m, MetadataM m) => CollectionName -> m ()
assertCollectionDefined = void . getCollectionDef
getCollectionDef ::
(QErrM m, MetadataM m) =>
CollectionName ->
m CreateCollection
getCollectionDef collName = do
detM <- getCollectionDefM collName
onNothing detM $
throw400 NotExists $
"query collection with name " <> collName <<> " does not exist"
getCollectionDefM ::
(QErrM m, MetadataM m) =>
CollectionName ->
m (Maybe CreateCollection)
getCollectionDefM collName =
OMap.lookup collName <$> fetchAllCollections
fetchAllCollections :: MetadataM m => m QueryCollections
fetchAllCollections =
_metaQueryCollections <$> getMetadata
fetchAllowlist :: MetadataM m => m MetadataAllowlist
fetchAllowlist = _metaAllowlist <$> getMetadata
fetchAllAllowlistCollections :: MetadataM m => m [CollectionName]
fetchAllAllowlistCollections = metadataAllowlistAllCollections <$> fetchAllowlist