mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-16 09:51:59 +03:00
cb8e6feb2e
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8121 GitOrigin-RevId: ceb3e29e330bba294061f85c1f75700974d01452
338 lines
12 KiB
Haskell
338 lines
12 KiB
Haskell
-- |
|
|
-- Description: Create/delete SQL functions to/from Hasura metadata.
|
|
module Hasura.Function.API
|
|
( FunctionPermissionArgument (..),
|
|
SetFunctionCustomization (..),
|
|
TrackFunction (..),
|
|
TrackFunctionV2 (..),
|
|
UnTrackFunction (..),
|
|
doesFunctionPermissionExist,
|
|
dropFunctionInMetadata,
|
|
dropFunctionPermissionInMetadata,
|
|
getSingleUniqueFunctionOverload,
|
|
runCreateFunctionPermission,
|
|
runDropFunctionPermission,
|
|
runSetFunctionCustomization,
|
|
runTrackFunc,
|
|
runTrackFunctionV2,
|
|
runUntrackFunc,
|
|
trackFunctionP1,
|
|
trackFunctionP2,
|
|
)
|
|
where
|
|
|
|
import Control.Lens ((.~), (^.))
|
|
import Data.Aeson
|
|
import Data.HashMap.Strict qualified as HashMap
|
|
import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
|
|
import Data.Text.Extended
|
|
import Hasura.Base.Error
|
|
import Hasura.EncJSON
|
|
import Hasura.Function.Cache
|
|
import Hasura.Function.Metadata (FunctionMetadata (..), fmConfiguration, fmPermissions)
|
|
import Hasura.Prelude
|
|
import Hasura.RQL.Types.Backend
|
|
import Hasura.RQL.Types.BackendTag
|
|
import Hasura.RQL.Types.BackendType
|
|
import Hasura.RQL.Types.Common
|
|
import Hasura.RQL.Types.Metadata
|
|
import Hasura.RQL.Types.Metadata.Backend
|
|
import Hasura.RQL.Types.Metadata.Instances ()
|
|
import Hasura.RQL.Types.Metadata.Object
|
|
import Hasura.RQL.Types.Roles (RoleName)
|
|
import Hasura.RQL.Types.SchemaCache
|
|
import Hasura.RQL.Types.SchemaCache.Build
|
|
import Hasura.SQL.AnyBackend qualified as AB
|
|
|
|
newtype TrackFunction b = TrackFunction {tfName :: FunctionName 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
|
|
unless (isJust $ AB.unpackAnyBackend @b =<< HashMap.lookup sourceName (scSources rawSchemaCache)) $
|
|
throw400 NotExists $
|
|
sourceName <<> " is not a known " <> reify (backendTag @b) <<> " source"
|
|
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 b ->
|
|
Maybe Text ->
|
|
m EncJSON
|
|
trackFunctionP2 sourceName qf config comment = do
|
|
buildSchemaCacheFor
|
|
(MOSourceObjId sourceName $ AB.mkAnyBackend $ SMOFunction @b qf)
|
|
$ MetadataModifier
|
|
$ metaSources . ix sourceName . toSourceMetadata . (smFunctions @b)
|
|
%~ InsOrdHashMap.insert qf (FunctionMetadata qf config mempty comment)
|
|
pure successMsg
|
|
|
|
getSingleUniqueFunctionOverload ::
|
|
forall b m.
|
|
(QErrM m, Backend b) =>
|
|
FunctionName b ->
|
|
FunctionOverloads b ->
|
|
m (RawFunctionInfo b)
|
|
getSingleUniqueFunctionOverload qf = \case
|
|
FunctionOverloads (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 Nothing
|
|
|
|
-- | JSON API payload for v2 of 'track_function':
|
|
--
|
|
-- https://hasura.io/docs/latest/graphql/core/api-reference/schema-metadata-api/custom-functions.html#track-function-v2
|
|
data TrackFunctionV2 (b :: BackendType) = TrackFunctionV2
|
|
{ _tfv2Source :: SourceName,
|
|
_tfv2Function :: FunctionName b,
|
|
_tfv2Configuration :: FunctionConfig b,
|
|
_tfv2Comment :: Maybe Text
|
|
}
|
|
|
|
instance Backend b => FromJSON (TrackFunctionV2 b) where
|
|
parseJSON = withObject "TrackFunctionV2" $ \o ->
|
|
TrackFunctionV2
|
|
<$> o .:? "source" .!= defaultSource
|
|
<*> o .: "function"
|
|
<*> o .:? "configuration" .!= emptyFunctionConfig
|
|
<*> o .:? "comment"
|
|
|
|
runTrackFunctionV2 ::
|
|
forall b m.
|
|
(BackendMetadata b, QErrM m, CacheRWM m, MetadataM m) =>
|
|
TrackFunctionV2 b ->
|
|
m EncJSON
|
|
runTrackFunctionV2 (TrackFunctionV2 source qf config comment) = do
|
|
trackFunctionP1 @b source qf
|
|
trackFunctionP2 @b source qf config comment
|
|
|
|
-- | 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
|
|
}
|
|
|
|
instance (Backend b) => FromJSON (UnTrackFunction b) where
|
|
-- Following was the previous implementation, which while seems to be correct,
|
|
-- has an unexpected behaviour. In the case when @source@ key is present but
|
|
-- @function@ key is absent, it would silently coerce it into a @default@
|
|
-- source. The culprint being the _alternative_ operator, which silently fails
|
|
-- the first parse. This note exists so that we don't try to simplify using
|
|
-- the _alternative_ pattern here.
|
|
-- Previous implementation :-
|
|
-- Consider the following JSON -
|
|
-- {
|
|
-- "source": "custom_source",
|
|
-- "schema": "public",
|
|
-- "name": "my_function"
|
|
-- }
|
|
-- it silently fails parsing the source here because @function@ key is not
|
|
-- present, and proceeds to parse using @withoutSource@ as default source. Now
|
|
-- this is surprising for the user, because they mention @source@ key
|
|
-- explicitly. A better behaviour is to explicitly look for @function@ key if
|
|
-- a @source@ key is present.
|
|
-- >>
|
|
-- parseJSON v = withSource <|> withoutSource
|
|
-- where
|
|
-- withoutSource = UnTrackFunction <$> parseJSON v <*> pure defaultSource
|
|
-- withSource = flip (withObject "UnTrackFunction") v \o -> do
|
|
-- UnTrackFunction <$> o .: "function"
|
|
-- <*> o .:? "source" .!= defaultSource
|
|
parseJSON v = flip (withObject "UnTrackFunction") v $ \o -> do
|
|
source <- o .:? "source"
|
|
case source of
|
|
Just src -> flip UnTrackFunction src <$> o .: "function"
|
|
Nothing -> UnTrackFunction <$> parseJSON v <*> pure defaultSource
|
|
|
|
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 sourceName functionName
|
|
pure successMsg
|
|
|
|
{- 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 FunctionPermissionArgument b = FunctionPermissionArgument
|
|
{ _afpFunction :: FunctionName b,
|
|
_afpSource :: SourceName,
|
|
_afpRole :: RoleName
|
|
}
|
|
|
|
instance (Backend b) => FromJSON (FunctionPermissionArgument b) where
|
|
parseJSON v =
|
|
flip (withObject "FunctionPermissionArgument") v $ \o ->
|
|
FunctionPermissionArgument
|
|
<$> o .: "function"
|
|
<*> o .:? "source" .!= defaultSource
|
|
<*> o .: "role"
|
|
|
|
runCreateFunctionPermission ::
|
|
forall b m.
|
|
( CacheRWM m,
|
|
MonadError QErr m,
|
|
MetadataM m,
|
|
BackendMetadata b
|
|
) =>
|
|
FunctionPermissionArgument b ->
|
|
m EncJSON
|
|
runCreateFunctionPermission (FunctionPermissionArgument functionName source role) = do
|
|
metadata <- getMetadata
|
|
sourceCache <- scSources <$> askSchemaCache
|
|
functionInfo <- askFunctionInfo @b source functionName
|
|
when (doesFunctionPermissionExist @b metadata source functionName role) $
|
|
throw400 AlreadyExists $
|
|
"permission of role "
|
|
<> role <<> " already exists for function "
|
|
<> functionName <<> " in source: " <>> source
|
|
(functionTableName, functionTableInfo) <- do
|
|
let tn = _fiReturnType functionInfo
|
|
case unsafeTableInfo @b source tn sourceCache of
|
|
Nothing -> throw400 NotExists ("function's return table " <> tn <<> " not found in the cache")
|
|
Just info -> pure (tn, info)
|
|
unless (role `HashMap.member` _tiRolePermInfoMap functionTableInfo) $
|
|
throw400 NotSupported $
|
|
"function permission can only be added when the function's return table "
|
|
<> functionTableName <<> " 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
|
|
%~ (:) (FunctionPermissionInfo 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)
|
|
|
|
doesFunctionPermissionExist :: forall b. (BackendMetadata b) => Metadata -> SourceName -> FunctionName b -> RoleName -> Bool
|
|
doesFunctionPermissionExist metadata sourceName functionName roleName =
|
|
any ((== roleName) . _fpmRole) $ metadata ^. (metaSources . ix sourceName . toSourceMetadata . (smFunctions @b) . ix functionName . fmPermissions)
|
|
|
|
runDropFunctionPermission ::
|
|
forall m b.
|
|
( CacheRWM m,
|
|
MonadError QErr m,
|
|
MetadataM m,
|
|
BackendMetadata b
|
|
) =>
|
|
FunctionPermissionArgument b ->
|
|
m EncJSON
|
|
runDropFunctionPermission (FunctionPermissionArgument functionName source role) = do
|
|
metadata <- getMetadata
|
|
unless (doesFunctionPermissionExist @b metadata source functionName role) $
|
|
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
|
|
|
|
-- | Represents the payload of the API command 'pg_set_function_customization'.
|
|
--
|
|
-- See the Hasura API reference for a detailed description.
|
|
data SetFunctionCustomization b = SetFunctionCustomization
|
|
{ _sfcSource :: SourceName,
|
|
_sfcFunction :: FunctionName b,
|
|
_sfcConfiguration :: FunctionConfig b
|
|
}
|
|
|
|
deriving instance Backend b => Show (SetFunctionCustomization b)
|
|
|
|
deriving instance Backend b => Eq (SetFunctionCustomization b)
|
|
|
|
instance (Backend b) => FromJSON (SetFunctionCustomization b) where
|
|
parseJSON = withObject "set function customization" $ \o ->
|
|
SetFunctionCustomization
|
|
<$> o .:? "source" .!= defaultSource
|
|
<*> o .: "function"
|
|
<*> o .: "configuration"
|
|
|
|
-- | Changes the custom names of a function. Used in the API command 'pg_set_function_customization'.
|
|
runSetFunctionCustomization ::
|
|
forall b m.
|
|
(QErrM m, CacheRWM m, MetadataM m, Backend b) =>
|
|
SetFunctionCustomization b ->
|
|
m EncJSON
|
|
runSetFunctionCustomization (SetFunctionCustomization source function config) = do
|
|
void $ askFunctionInfo @b source function
|
|
buildSchemaCacheFor
|
|
(MOSourceObjId source $ AB.mkAnyBackend $ SMOFunction @b function)
|
|
$ MetadataModifier
|
|
$ ((functionMetadataSetter @b source function) . fmConfiguration) .~ config
|
|
return successMsg
|