graphql-engine/server/src-lib/Hasura/RQL/DDL/Schema/Function.hs

246 lines
8.8 KiB
Haskell
Raw Normal View History

{- |
Description: Create/delete SQL functions to/from Hasura metadata.
-}
module Hasura.RQL.DDL.Schema.Function where
import Hasura.Prelude
2020-08-27 19:36:39 +03:00
import qualified Data.HashMap.Strict as Map
import qualified Data.HashMap.Strict.InsOrd as OMap
import Data.Aeson
import Data.Text.Extended
import qualified Hasura.SQL.AnyBackend as AB
2020-08-27 19:36:39 +03:00
import Hasura.EncJSON
import Hasura.RQL.Types
import Hasura.Session
newtype TrackFunction b
= TrackFunction
{ tfName :: FunctionName b }
deriving instance (Backend b) => Show (TrackFunction b)
deriving instance (Backend b) => Eq (TrackFunction b)
deriving instance (Backend b) => FromJSON (TrackFunction b)
deriving instance (Backend b) => ToJSON (TrackFunction b)
-- | Track function, Phase 1:
-- Validate function tracking operation. Fails if function is already being
-- tracked, or if a table with the same name is being tracked.
trackFunctionP1
:: forall b m
. (CacheRM m, QErrM m, Backend b)
=> SourceName
-> FunctionName b
-> m ()
trackFunctionP1 sourceName qf = do
rawSchemaCache <- askSchemaCache
when (isJust $ unsafeFunctionInfo @b sourceName qf $ scSources rawSchemaCache) $
throw400 AlreadyTracked $ "function already tracked : " <>> qf
let qt = functionToTable @b qf
when (isJust $ unsafeTableInfo @b sourceName qt $ scSources rawSchemaCache) $
throw400 NotSupported $ "table with name " <> qf <<> " already exists"
trackFunctionP2
:: forall b m
. (MonadError QErr m, CacheRWM m, MetadataM m, BackendMetadata b)
=> SourceName -> FunctionName b -> FunctionConfig -> m EncJSON
trackFunctionP2 sourceName qf config = do
buildSchemaCacheFor
(MOSourceObjId sourceName $ AB.mkAnyBackend $ SMOFunction @b qf)
$ MetadataModifier
$ metaSources.ix sourceName.toSourceMetadata.(smFunctions @b)
%~ OMap.insert qf (FunctionMetadata qf config mempty)
pure successMsg
handleMultipleFunctions
:: forall b m a
. (QErrM m, Backend b)
=> FunctionName b
-> [a]
-> m a
handleMultipleFunctions qf = \case
[] ->
throw400 NotExists $ "no such function exists in postgres : " <>> qf
[fi] -> return fi
_ ->
throw400 NotSupported $
"function " <> qf <<> " is overloaded. Overloaded functions are not supported"
runTrackFunc
:: forall b m
. (MonadError QErr m, CacheRWM m, MetadataM m, BackendMetadata b)
=> TrackFunction b
-> m EncJSON
runTrackFunc (TrackFunction qf) = do
-- v1 track_function lacks a means to take extra arguments
trackFunctionP1 @b defaultSource qf
trackFunctionP2 @b defaultSource qf emptyFunctionConfig
runTrackFunctionV2
:: forall b m
. (BackendMetadata b, QErrM m, CacheRWM m, MetadataM m)
=> TrackFunctionV2 b
-> m EncJSON
runTrackFunctionV2 (TrackFunctionV2 source qf config) = do
trackFunctionP1 @b source qf
trackFunctionP2 @b source qf config
-- | JSON API payload for 'untrack_function':
--
-- https://hasura.io/docs/latest/graphql/core/api-reference/schema-metadata-api/custom-functions.html#untrack-function
data UnTrackFunction b
= UnTrackFunction
{ _utfFunction :: !(FunctionName b)
, _utfSource :: !SourceName
} deriving (Generic)
deriving instance (Backend b) => Show (UnTrackFunction b)
deriving instance (Backend b) => Eq (UnTrackFunction b)
instance (Backend b) => ToJSON (UnTrackFunction b) where
toJSON = genericToJSON hasuraJSON
instance (Backend b) => FromJSON (UnTrackFunction b) where
parseJSON v = withSource <|> withoutSource
where
withoutSource = UnTrackFunction <$> parseJSON v <*> pure defaultSource
withSource = flip (withObject "UnTrackFunction") v \o ->
UnTrackFunction <$> o .: "function"
<*> o .:? "source" .!= defaultSource
askFunctionInfo
:: forall b m
. (CacheRM m, MonadError QErr m, Backend b)
=> SourceName -> FunctionName b -> m (FunctionInfo b)
askFunctionInfo source functionName = do
sourceCache <- scSources <$> askSchemaCache
unsafeFunctionInfo @b source functionName sourceCache
`onNothing` throw400 NotExists ("function " <> functionName <<> " not found in the cache")
runUntrackFunc
:: forall b m
. (CacheRWM m, MonadError QErr m, MetadataM m, BackendMetadata b)
=> UnTrackFunction b
-> m EncJSON
runUntrackFunc (UnTrackFunction functionName sourceName) = do
void $ askFunctionInfo @b sourceName functionName
withNewInconsistentObjsCheck
$ buildSchemaCache
$ dropFunctionInMetadata @b defaultSource functionName
pure successMsg
dropFunctionInMetadata
:: forall b
. (BackendMetadata b)
=> SourceName
-> FunctionName b
-> MetadataModifier
dropFunctionInMetadata source function = MetadataModifier $
metaSources.ix source.toSourceMetadata.(smFunctions @b) %~ OMap.delete function
{- Note [Function Permissions]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Before we started supporting tracking volatile functions, permissions
for a function was inferred from the target table of the function.
The rationale behind this is that a stable/immutable function does not
modify the database and the data returned by the function is filtered using
the permissions that are specified precisely for that data.
Now consider mutable/volatile functions, we can't automatically infer whether or
not these functions should be exposed for the sole reason that they can modify
the database. This necessitates a permission system for functions.
So, we introduce a new API `pg_create_function_permission` which will
explicitly grant permission to a function to a role. For creating a
function permission, the role must have select permissions configured
for the target table.
Since, this is a breaking change, we enable it only when the graphql-engine
is started with
`--infer-function-permissions`/HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS set
to false (by default, it's set to true).
-}
data CreateFunctionPermission b
= CreateFunctionPermission
{ _afpFunction :: !(FunctionName b)
, _afpSource :: !SourceName
, _afpRole :: !RoleName
} deriving (Generic)
deriving instance (Backend b) => Show (CreateFunctionPermission b)
deriving instance (Backend b) => Eq (CreateFunctionPermission b)
instance (Backend b) => ToJSON (CreateFunctionPermission b) where
toJSON = genericToJSON hasuraJSON
instance (Backend b) => FromJSON (CreateFunctionPermission b) where
parseJSON v =
flip (withObject "CreateFunctionPermission") v $ \o ->
CreateFunctionPermission
<$> o .: "function"
<*> o .:? "source" .!= defaultSource
<*> o .: "role"
runCreateFunctionPermission
:: forall b m
. ( CacheRWM m
, MonadError QErr m
, MetadataM m
, BackendMetadata b
)
=> CreateFunctionPermission b
-> m EncJSON
runCreateFunctionPermission (CreateFunctionPermission functionName source role) = do
sourceCache <- scSources <$> askSchemaCache
functionInfo <- askFunctionInfo @b source functionName
when (role `elem` _fiPermissions functionInfo) $
throw400 AlreadyExists $
"permission of role "
<> role <<> " already exists for function " <> functionName <<> " in source: " <>> source
functionTableInfo <-
unsafeTableInfo @b source (_fiReturnType functionInfo) sourceCache
`onNothing` throw400 NotExists ("function's return table " <> _fiReturnType functionInfo <<> " not found in the cache")
unless (role `Map.member` _tiRolePermInfoMap functionTableInfo) $
throw400 NotSupported $
"function permission can only be added when the function's return table "
<> _fiReturnType functionInfo <<> " has select permission configured for role: " <>> role
buildSchemaCacheFor
(MOSourceObjId source
$ AB.mkAnyBackend (SMOFunctionPermission @b functionName role))
$ MetadataModifier
$ metaSources.ix
source.toSourceMetadata.(smFunctions @b).ix functionName.fmPermissions
%~ (:) (FunctionPermissionMetadata role)
pure successMsg
dropFunctionPermissionInMetadata
:: forall b
. (BackendMetadata b)
=> SourceName
-> FunctionName b
-> RoleName
-> MetadataModifier
dropFunctionPermissionInMetadata source function role = MetadataModifier $
metaSources.ix source.toSourceMetadata.(smFunctions @b).ix function.fmPermissions %~ filter ((/=) role . _fpmRole)
type DropFunctionPermission = CreateFunctionPermission
runDropFunctionPermission
:: forall m b
. ( CacheRWM m
, MonadError QErr m
, MetadataM m
, BackendMetadata b
)
=> DropFunctionPermission b
-> m EncJSON
runDropFunctionPermission (CreateFunctionPermission functionName source role) = do
functionInfo <- askFunctionInfo @b source functionName
unless (role `elem` _fiPermissions functionInfo) $
throw400 NotExists $
"permission of role "
<> role <<> " does not exist for function " <> functionName <<> " in source: " <>> source
buildSchemaCacheFor
(MOSourceObjId source
$ AB.mkAnyBackend
$ SMOFunctionPermission @b functionName role)
$ dropFunctionPermissionInMetadata @b source functionName role
pure successMsg