graphql-engine/server/src-lib/Hasura/RQL/DDL/Schema/LegacyCatalog.hs
Karthikeyan Chinnakonda 92026b769f [Preview] Inherited roles for postgres read queries
fixes #3868

docker image - `hasura/graphql-engine:inherited-roles-preview-48b73a2de`

Note:

To be able to use the inherited roles feature, the graphql-engine should be started with the env variable `HASURA_GRAPHQL_EXPERIMENTAL_FEATURES` set to `inherited_roles`.

Introduction
------------

This PR implements the idea of multiple roles as presented in this [paper](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/FGALanguageICDE07.pdf). The multiple roles feature in this PR can be used via inherited roles. An inherited role is a role which can be created by combining multiple singular roles. For example, if there are two roles `author` and `editor` configured in the graphql-engine, then we can create a inherited role with the name of `combined_author_editor` role which will combine the select permissions of the `author` and `editor` roles and then make GraphQL queries using the `combined_author_editor`.

How are select permissions of different roles are combined?
------------------------------------------------------------

A select permission includes 5 things:

1. Columns accessible to the role
2. Row selection filter
3. Limit
4. Allow aggregation
5. Scalar computed fields accessible to the role

 Suppose there are two roles, `role1` gives access to the `address` column with row filter `P1` and `role2` gives access to both the `address` and the `phone` column with row filter `P2` and we create a new role `combined_roles` which combines `role1` and `role2`.

Let's say the following GraphQL query is queried with the `combined_roles` role.

```graphql
query {
   employees {
     address
     phone
   }
}
```

This will translate to the following SQL query:

```sql

 select
    (case when (P1 or P2) then address else null end) as address,
    (case when P2 then phone else null end) as phone
 from employee
 where (P1 or P2)
```

The other parameters of the select permission will be combined in the following manner:

1. Limit - Minimum of the limits will be the limit of the inherited role
2. Allow aggregations - If any of the role allows aggregation, then the inherited role will allow aggregation
3. Scalar computed fields - same as table column fields, as in the above example

APIs for inherited roles:
----------------------

1. `add_inherited_role`

`add_inherited_role` is the [metadata API](https://hasura.io/docs/1.0/graphql/core/api-reference/index.html#schema-metadata-api) to create a new inherited role. It accepts two arguments

`role_name`: the name of the inherited role to be added (String)
`role_set`: list of roles that need to be combined (Array of Strings)

Example:

```json
{
  "type": "add_inherited_role",
  "args": {
      "role_name":"combined_user",
      "role_set":[
          "user",
          "user1"
      ]
  }
}
```

After adding the inherited role, the inherited role can be used like single roles like earlier

Note:

An inherited role can only be created with non-inherited/singular roles.

2. `drop_inherited_role`

The `drop_inherited_role` API accepts the name of the inherited role and drops it from the metadata. It accepts a single argument:

`role_name`: name of the inherited role to be dropped

Example:

```json

{
  "type": "drop_inherited_role",
  "args": {
      "role_name":"combined_user"
  }
}
```

Metadata
---------

The derived roles metadata will be included under the `experimental_features` key while exporting the metadata.

```json
{
  "experimental_features": {
    "derived_roles": [
      {
        "role_name": "manager_is_employee_too",
        "role_set": [
          "employee",
          "manager"
        ]
      }
    ]
  }
}
```

Scope
------

Only postgres queries and subscriptions are supported in this PR.

Important points:
-----------------

1. All columns exposed to an inherited role will be marked as `nullable`, this is done so that cell value nullification can be done.

TODOs
-------

- [ ] Tests
   - [ ] Test a GraphQL query running with a inherited role without enabling inherited roles in experimental features
   - [] Tests for aggregate queries, limit, computed fields, functions, subscriptions (?)
   - [ ] Introspection test with a inherited role (nullability changes in a inherited role)
- [ ] Docs
- [ ] Changelog

Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com>
GitOrigin-RevId: 3b8ee1e11f5ceca80fe294f8c074d42fbccfec63
2021-03-08 11:15:10 +00:00

663 lines
28 KiB
Haskell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- | Funtions related to @hdb_catalog@ schema prior to metadata separation (catalog version < 43).
module Hasura.RQL.DDL.Schema.LegacyCatalog
( saveMetadataToHdbTables
, fetchMetadataFromHdbTables
, recreateSystemMetadata
) where
import Hasura.Prelude
import qualified Data.HashMap.Strict as HM
import qualified Data.HashMap.Strict.InsOrd as OMap
import qualified Data.HashSet.InsOrd as HSIns
import qualified Data.Time.Clock as C
import qualified Database.PG.Query as Q
import Control.Lens hiding ((.=))
import Data.Aeson
import Data.Text.NonEmpty
import Hasura.Backends.Postgres.Connection
import Hasura.Backends.Postgres.SQL.Types
import Hasura.Eventing.ScheduledTrigger
import Hasura.RQL.DDL.ComputedField
import Hasura.RQL.DDL.Permission
import Hasura.RQL.Types
saveMetadataToHdbTables
:: (MonadTx m, HasSystemDefined m) => MetadataNoSources -> m ()
saveMetadataToHdbTables (MetadataNoSources tables functions schemas collections
allowlist customTypes actions cronTriggers) = do
withPathK "tables" $ do
indexedForM_ tables $ \TableMetadata{..} -> do
-- Save table
saveTableToCatalog _tmTable _tmIsEnum _tmConfiguration
-- Relationships
withPathK "object_relationships" $
indexedForM_ _tmObjectRelationships $ \objRel ->
insertRelationshipToCatalog _tmTable ObjRel objRel
withPathK "array_relationships" $
indexedForM_ _tmArrayRelationships $ \arrRel ->
insertRelationshipToCatalog _tmTable ArrRel arrRel
-- Computed Fields
withPathK "computed_fields" $
indexedForM_ _tmComputedFields $
\(ComputedFieldMetadata name definition comment) ->
addComputedFieldToCatalog $
AddComputedField defaultSource _tmTable name definition comment
-- Remote Relationships
withPathK "remote_relationships" $
indexedForM_ _tmRemoteRelationships $
\(RemoteRelationshipMetadata name def) -> do
let RemoteRelationshipDef rs hf rf = def
addRemoteRelationshipToCatalog $
RemoteRelationship name defaultSource _tmTable hf rs rf
-- Permissions
withPathK "insert_permissions" $ processPerms _tmTable _tmInsertPermissions
withPathK "select_permissions" $ processPerms _tmTable _tmSelectPermissions
withPathK "update_permissions" $ processPerms _tmTable _tmUpdatePermissions
withPathK "delete_permissions" $ processPerms _tmTable _tmDeletePermissions
-- Event triggers
withPathK "event_triggers" $
indexedForM_ _tmEventTriggers $ \etc -> addEventTriggerToCatalog _tmTable etc
-- sql functions
withPathK "functions" $ indexedForM_ functions $
\(FunctionMetadata function config _) -> addFunctionToCatalog function config
-- query collections
systemDefined <- askSystemDefined
withPathK "query_collections" $
indexedForM_ collections $ \c -> liftTx $ addCollectionToCatalog c systemDefined
-- allow list
withPathK "allowlist" $ do
indexedForM_ allowlist $ \(CollectionReq name) ->
liftTx $ addCollectionToAllowlistCatalog name
-- remote schemas
withPathK "remote_schemas" $
indexedMapM_ (liftTx . addRemoteSchemaToCatalog) schemas
-- custom types
withPathK "custom_types" $ setCustomTypesInCatalog customTypes
-- cron triggers
withPathK "cron_triggers" $
indexedForM_ cronTriggers $ \ct -> liftTx $ do
addCronTriggerToCatalog ct
-- actions
withPathK "actions" $
indexedForM_ actions $ \action -> do
let createAction =
CreateAction (_amName action) (_amDefinition action) (_amComment action)
addActionToCatalog createAction
withPathK "permissions" $
indexedForM_ (_amPermissions action) $ \permission -> do
let createActionPermission = CreateActionPermission (_amName action)
(_apmRole permission) Nothing (_apmComment permission)
addActionPermissionToCatalog createActionPermission
where
processPerms tableName perms = indexedForM_ perms $ \perm -> do
let pt = permAccToType @'Postgres $ getPermAcc1 perm
systemDefined <- askSystemDefined
liftTx $ addPermissionToCatalog pt tableName perm systemDefined
saveTableToCatalog
:: (MonadTx m, HasSystemDefined m) => QualifiedTable -> Bool -> TableConfig 'Postgres -> m ()
saveTableToCatalog (QualifiedObject sn tn) isEnum config = do
systemDefined <- askSystemDefined
liftTx $ Q.unitQE defaultTxErrorHandler [Q.sql|
INSERT INTO "hdb_catalog"."hdb_table"
(table_schema, table_name, is_system_defined, is_enum, configuration)
VALUES ($1, $2, $3, $4, $5)
|] (sn, tn, systemDefined, isEnum, configVal) False
where
configVal = Q.AltJ $ toJSON config
insertRelationshipToCatalog
:: (MonadTx m, HasSystemDefined m, ToJSON a)
=> QualifiedTable
-> RelType
-> RelDef a
-> m ()
insertRelationshipToCatalog (QualifiedObject schema table) relType (RelDef name using comment) = do
systemDefined <- askSystemDefined
let args = (schema, table, name, relTypeToTxt relType, Q.AltJ using, comment, systemDefined)
liftTx $ Q.unitQE defaultTxErrorHandler query args True
where
query = [Q.sql|
INSERT INTO
hdb_catalog.hdb_relationship
(table_schema, table_name, rel_name, rel_type, rel_def, comment, is_system_defined)
VALUES ($1, $2, $3, $4, $5 :: jsonb, $6, $7) |]
addEventTriggerToCatalog
:: MonadTx m
=> QualifiedTable
-> EventTriggerConf
-> m ()
addEventTriggerToCatalog qt etc = liftTx do
Q.unitQE defaultTxErrorHandler
[Q.sql|
INSERT into hdb_catalog.event_triggers
(name, type, schema_name, table_name, configuration)
VALUES ($1, 'table', $2, $3, $4)
|] (name, sn, tn, Q.AltJ $ toJSON etc) False
where
QualifiedObject sn tn = qt
(EventTriggerConf name _ _ _ _ _) = etc
addComputedFieldToCatalog
:: MonadTx m
=> AddComputedField 'Postgres -> m ()
addComputedFieldToCatalog q =
liftTx $ Q.withQE defaultTxErrorHandler
[Q.sql|
INSERT INTO hdb_catalog.hdb_computed_field
(table_schema, table_name, computed_field_name, definition, comment)
VALUES ($1, $2, $3, $4, $5)
|] (schemaName, tableName, computedField, Q.AltJ definition, comment) True
where
QualifiedObject schemaName tableName = table
AddComputedField _ table computedField definition comment = q
addRemoteRelationshipToCatalog :: MonadTx m => RemoteRelationship 'Postgres -> m ()
addRemoteRelationshipToCatalog remoteRelationship = liftTx $
Q.unitQE defaultTxErrorHandler [Q.sql|
INSERT INTO hdb_catalog.hdb_remote_relationship
(remote_relationship_name, table_schema, table_name, definition)
VALUES ($1, $2, $3, $4::jsonb)
|] (rtrName remoteRelationship, schemaName, tableName, Q.AltJ definition) True
where
QualifiedObject schemaName tableName = rtrTable remoteRelationship
definition = mkRemoteRelationshipDef remoteRelationship
mkRemoteRelationshipDef RemoteRelationship {..} =
RemoteRelationshipDef rtrRemoteSchema rtrHasuraFields rtrRemoteField
addFunctionToCatalog
:: (MonadTx m, HasSystemDefined m)
=> QualifiedFunction -> FunctionConfig -> m ()
addFunctionToCatalog (QualifiedObject sn fn) config = do
systemDefined <- askSystemDefined
liftTx $ Q.unitQE defaultTxErrorHandler [Q.sql|
INSERT INTO "hdb_catalog"."hdb_function"
(function_schema, function_name, configuration, is_system_defined)
VALUES ($1, $2, $3, $4)
|] (sn, fn, Q.AltJ config, systemDefined) False
addRemoteSchemaToCatalog
:: RemoteSchemaMetadata
-> Q.TxE QErr ()
addRemoteSchemaToCatalog (RemoteSchemaMetadata name def comment _) =
Q.unitQE defaultTxErrorHandler [Q.sql|
INSERT into hdb_catalog.remote_schemas
(name, definition, comment)
VALUES ($1, $2, $3)
|] (name, Q.AltJ $ toJSON def, comment) True
addCollectionToCatalog
:: MonadTx m => CreateCollection -> SystemDefined -> m ()
addCollectionToCatalog (CreateCollection name defn mComment) systemDefined = liftTx $
Q.unitQE defaultTxErrorHandler [Q.sql|
INSERT INTO hdb_catalog.hdb_query_collection
(collection_name, collection_defn, comment, is_system_defined)
VALUES ($1, $2, $3, $4)
|] (name, Q.AltJ defn, mComment, systemDefined) True
addCollectionToAllowlistCatalog :: MonadTx m => CollectionName -> m ()
addCollectionToAllowlistCatalog collName = liftTx $
Q.unitQE defaultTxErrorHandler [Q.sql|
INSERT INTO hdb_catalog.hdb_allowlist
(collection_name)
VALUES ($1)
|] (Identity collName) True
setCustomTypesInCatalog :: MonadTx m => CustomTypes -> m ()
setCustomTypesInCatalog customTypes = liftTx do
clearCustomTypes
Q.unitQE defaultTxErrorHandler [Q.sql|
INSERT into hdb_catalog.hdb_custom_types
(custom_types)
VALUES ($1)
|] (Identity $ Q.AltJ customTypes) False
where
clearCustomTypes = do
Q.unitQE defaultTxErrorHandler [Q.sql|
DELETE FROM hdb_catalog.hdb_custom_types
|] () False
addActionToCatalog :: (MonadTx m) => CreateAction -> m ()
addActionToCatalog (CreateAction actionName actionDefinition comment) = do
liftTx $ Q.unitQE defaultTxErrorHandler [Q.sql|
INSERT into hdb_catalog.hdb_action
(action_name, action_defn, comment)
VALUES ($1, $2, $3)
|] (actionName, Q.AltJ actionDefinition, comment) True
addActionPermissionToCatalog :: (MonadTx m) => CreateActionPermission -> m ()
addActionPermissionToCatalog CreateActionPermission{..}= do
liftTx $ Q.unitQE defaultTxErrorHandler [Q.sql|
INSERT into hdb_catalog.hdb_action_permission
(action_name, role_name, comment)
VALUES ($1, $2, $3)
|] (_capAction, _capRole, _capComment) True
addPermissionToCatalog
:: (ToJSON a, MonadTx m)
=> PermType
-> QualifiedTable
-> PermDef a
-> SystemDefined
-> m ()
addPermissionToCatalog pt (QualifiedObject sn tn) (PermDef rn qdef mComment) systemDefined = liftTx $
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
addCronTriggerToCatalog :: (MonadTx m) => CronTriggerMetadata -> m ()
addCronTriggerToCatalog CronTriggerMetadata {..} = liftTx $ do
Q.unitQE defaultTxErrorHandler
[Q.sql|
INSERT into hdb_catalog.hdb_cron_triggers
(name, webhook_conf, cron_schedule, payload, retry_conf, header_conf, include_in_metadata, comment)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|] (ctName, Q.AltJ ctWebhook, ctSchedule, Q.AltJ <$> ctPayload, Q.AltJ ctRetryConf
,Q.AltJ ctHeaders, ctIncludeInMetadata, ctComment) False
currentTime <- liftIO C.getCurrentTime
let scheduleTimes = generateScheduleTimes currentTime 100 ctSchedule -- generate next 100 events
insertScheduledEventTx $ SESCron $ map (CronEventSeed ctName) scheduleTimes
fetchMetadataFromHdbTables :: MonadTx m => m MetadataNoSources
fetchMetadataFromHdbTables = liftTx do
tables <- Q.catchE defaultTxErrorHandler fetchTables
let tableMetaMap = OMap.fromList . flip map tables $
\(schema, name, isEnum, maybeConfig) ->
let qualifiedName = QualifiedObject schema name
configuration = maybe emptyTableConfig Q.getAltJ maybeConfig
in (qualifiedName, mkTableMeta qualifiedName isEnum configuration)
-- Fetch all the relationships
relationships <- Q.catchE defaultTxErrorHandler fetchRelationships
objRelDefs <- mkRelDefs ObjRel relationships
arrRelDefs <- mkRelDefs ArrRel relationships
-- Fetch all the permissions
permissions <- Q.catchE defaultTxErrorHandler fetchPermissions
-- Parse all the permissions
insPermDefs <- mkPermDefs PTInsert permissions
selPermDefs <- mkPermDefs PTSelect permissions
updPermDefs <- mkPermDefs PTUpdate permissions
delPermDefs <- mkPermDefs PTDelete permissions
-- Fetch all event triggers
eventTriggers <- Q.catchE defaultTxErrorHandler fetchEventTriggers
triggerMetaDefs <- mkTriggerMetaDefs eventTriggers
-- Fetch all computed fields
computedFields <- fetchComputedFields
-- Fetch all remote relationships
remoteRelationships <- Q.catchE defaultTxErrorHandler fetchRemoteRelationships
let (_, fullTableMetaMap) = flip runState tableMetaMap $ do
modMetaMap tmObjectRelationships _rdName objRelDefs
modMetaMap tmArrayRelationships _rdName arrRelDefs
modMetaMap tmInsertPermissions _pdRole insPermDefs
modMetaMap tmSelectPermissions _pdRole selPermDefs
modMetaMap tmUpdatePermissions _pdRole updPermDefs
modMetaMap tmDeletePermissions _pdRole delPermDefs
modMetaMap tmEventTriggers etcName triggerMetaDefs
modMetaMap tmComputedFields _cfmName computedFields
modMetaMap tmRemoteRelationships _rrmName remoteRelationships
-- fetch all functions
functions <- Q.catchE defaultTxErrorHandler fetchFunctions
-- fetch all remote schemas
remoteSchemas <- oMapFromL _rsmName <$> fetchRemoteSchemas
-- fetch all collections
collections <- oMapFromL _ccName <$> fetchCollections
-- fetch allow list
allowlist <- HSIns.fromList . map CollectionReq <$> fetchAllowlists
customTypes <- fetchCustomTypes
-- fetch actions
actions <- oMapFromL _amName <$> fetchActions
MetadataNoSources fullTableMetaMap functions remoteSchemas collections
allowlist customTypes actions <$> fetchCronTriggers
where
modMetaMap l f xs = do
st <- get
put $ foldl' (\b (qt, dfn) -> b & at qt._Just.l %~ OMap.insert (f dfn) dfn) st xs
mkPermDefs pt = mapM permRowToDef . filter (\pr -> pr ^. _4 == pt)
permRowToDef (sn, tn, rn, _, Q.AltJ pDef, mComment) = do
perm <- decodeValue pDef
return (QualifiedObject sn tn, PermDef rn perm mComment)
mkRelDefs rt = mapM relRowToDef . filter (\rr -> rr ^. _4 == rt)
relRowToDef (sn, tn, rn, _, Q.AltJ rDef, mComment) = do
using <- decodeValue rDef
return (QualifiedObject sn tn, RelDef rn using mComment)
mkTriggerMetaDefs = mapM trigRowToDef
trigRowToDef (sn, tn, Q.AltJ configuration) = do
conf <- decodeValue configuration
return (QualifiedObject sn tn, conf::EventTriggerConf)
fetchTables =
Q.listQ [Q.sql|
SELECT table_schema, table_name, is_enum, configuration::json
FROM hdb_catalog.hdb_table
WHERE is_system_defined = 'false'
ORDER BY table_schema ASC, table_name ASC
|] () False
fetchRelationships =
Q.listQ [Q.sql|
SELECT table_schema, table_name, rel_name, rel_type, rel_def::json, comment
FROM hdb_catalog.hdb_relationship
WHERE is_system_defined = 'false'
ORDER BY table_schema ASC, table_name ASC, rel_name ASC
|] () False
fetchPermissions =
Q.listQ [Q.sql|
SELECT table_schema, table_name, role_name, perm_type, perm_def::json, comment
FROM hdb_catalog.hdb_permission
WHERE is_system_defined = 'false'
ORDER BY table_schema ASC, table_name ASC, role_name ASC, perm_type ASC
|] () False
fetchEventTriggers =
Q.listQ [Q.sql|
SELECT e.schema_name, e.table_name, e.configuration::json
FROM hdb_catalog.event_triggers e
ORDER BY e.schema_name ASC, e.table_name ASC, e.name ASC
|] () False
fetchFunctions = do
l <- Q.listQ [Q.sql|
SELECT function_schema, function_name, configuration::json
FROM hdb_catalog.hdb_function
WHERE is_system_defined = 'false'
ORDER BY function_schema ASC, function_name ASC
|] () False
pure $ oMapFromL _fmFunction $
flip map l $ \(sn, fn, Q.AltJ config) ->
-- function permissions were only introduced post 43rd
-- migration, so it's impossible we get any permissions
-- here
FunctionMetadata (QualifiedObject sn fn) config []
fetchRemoteSchemas =
map fromRow <$> Q.listQE defaultTxErrorHandler
[Q.sql|
SELECT name, definition, comment
FROM hdb_catalog.remote_schemas
ORDER BY name ASC
|] () True
where
fromRow (name, Q.AltJ def, comment) =
RemoteSchemaMetadata name def comment mempty
fetchCollections =
map fromRow <$> Q.listQE defaultTxErrorHandler [Q.sql|
SELECT collection_name, collection_defn::json, comment
FROM hdb_catalog.hdb_query_collection
WHERE is_system_defined = 'false'
ORDER BY collection_name ASC
|] () False
where
fromRow (name, Q.AltJ defn, mComment) =
CreateCollection name defn mComment
fetchAllowlists = map runIdentity <$>
Q.listQE defaultTxErrorHandler [Q.sql|
SELECT collection_name
FROM hdb_catalog.hdb_allowlist
ORDER BY collection_name ASC
|] () False
fetchComputedFields = do
r <- Q.listQE defaultTxErrorHandler [Q.sql|
SELECT table_schema, table_name, computed_field_name,
definition::json, comment
FROM hdb_catalog.hdb_computed_field
|] () False
pure $ flip map r $ \(schema, table, name, Q.AltJ definition, comment) ->
( QualifiedObject schema table
, ComputedFieldMetadata name definition comment
)
fetchCronTriggers =
(oMapFromL ctName . map uncurryCronTrigger)
<$> Q.listQE defaultTxErrorHandler
[Q.sql|
SELECT ct.name, ct.webhook_conf, ct.cron_schedule, ct.payload,
ct.retry_conf, ct.header_conf, ct.include_in_metadata, ct.comment
FROM hdb_catalog.hdb_cron_triggers ct
WHERE include_in_metadata
|] () False
where
uncurryCronTrigger
(name, webhook, schedule, payload, retryConfig, headerConfig, includeMetadata, comment) =
CronTriggerMetadata
{ ctName = name,
ctWebhook = Q.getAltJ webhook,
ctSchedule = schedule,
ctPayload = Q.getAltJ <$> payload,
ctRetryConf = Q.getAltJ retryConfig,
ctHeaders = Q.getAltJ headerConfig,
ctIncludeInMetadata = includeMetadata,
ctComment = comment
}
fetchCustomTypes :: Q.TxE QErr CustomTypes
fetchCustomTypes =
Q.getAltJ . runIdentity . Q.getRow <$>
Q.rawQE defaultTxErrorHandler [Q.sql|
select coalesce((select custom_types::json from hdb_catalog.hdb_custom_types), '{}'::json)
|] [] False
fetchActions =
Q.getAltJ . runIdentity . Q.getRow <$> Q.rawQE defaultTxErrorHandler [Q.sql|
select
coalesce(
json_agg(
json_build_object(
'name', a.action_name,
'definition', a.action_defn,
'comment', a.comment,
'permissions', ap.permissions
) order by a.action_name asc
),
'[]'
)
from
hdb_catalog.hdb_action as a
left outer join lateral (
select
coalesce(
json_agg(
json_build_object(
'role', ap.role_name,
'comment', ap.comment
) order by ap.role_name asc
),
'[]'
) as permissions
from
hdb_catalog.hdb_action_permission ap
where
ap.action_name = a.action_name
) ap on true;
|] [] False
fetchRemoteRelationships = do
r <- Q.listQ [Q.sql|
SELECT table_schema, table_name,
remote_relationship_name, definition::json
FROM hdb_catalog.hdb_remote_relationship
|] () False
pure $ flip map r $ \(schema, table, name, Q.AltJ definition) ->
( QualifiedObject schema table
, RemoteRelationshipMetadata name definition
)
-- | Drops and recreates all “system-defined” metadata, aka metadata for tables and views in the
-- @information_schema@ and @hdb_catalog@ schemas. These tables and views are tracked to expose them
-- to the console, which allows us to reuse the same functionality we use to implement user-defined
-- APIs to expose the catalog.
--
-- This process has a long and storied history.
--
-- In the past, we reused the same machinery we use for CLI migrations to define our own internal
-- metadata migrations. This caused trouble, however, as wed have to run those migrations in
-- lockstep with our SQL migrations to ensure the two didnt get out of sync. This in turn caused
-- trouble because those migrations would hit code paths inside @graphql-engine@ to add or remove
-- things from the @pg_catalog@ tables, and /that/ in turn would fail because we hadnt finished
-- running the SQL migrations, so we were running a new version of the code against an old version
-- of the schema! That caused #2826.
--
-- To fix that, #2379 switched to the approach of just dropping and recreating all system metadata
-- every time we run any SQL migrations. But /that/ in turn caused trouble due to the way we were
-- constantly rebuilding the schema cache (#3354), causing us to switch to incremental schema cache
-- construction (#3394). However, although that mostly resolved the problem, we still werent
-- totally out of the woods, as the incremental construction was still too slow on slow Postgres
-- instances (#3654).
--
-- To sidestep the whole issue, as of #3686 we now just create all the system metadata in code here,
-- and we only rebuild the schema cache once, at the very end. This is a little unsatisfying, since
-- it means our internal migrations are “blessed” compared to user-defined CLI migrations. If we
-- improve CLI migrations further in the future, maybe we can switch back to using that approach,
-- instead.
recreateSystemMetadata :: (MonadTx m) => m ()
recreateSystemMetadata = do
() <- liftTx $ Q.multiQE defaultTxErrorHandler $(Q.sqlFromFile "src-rsr/clear_system_metadata.sql")
runHasSystemDefinedT (SystemDefined True) $ for_ systemMetadata \(tableName, tableRels) -> do
saveTableToCatalog tableName False emptyTableConfig
for_ tableRels \case
Left relDef -> insertRelationshipToCatalog tableName ObjRel relDef
Right relDef -> insertRelationshipToCatalog tableName ArrRel relDef
where
systemMetadata :: [(QualifiedTable, [Either (ObjRelDef 'Postgres) (ArrRelDef 'Postgres)])]
systemMetadata =
[ table "information_schema" "tables" []
, table "information_schema" "schemata" []
, table "information_schema" "views" []
, table "information_schema" "columns" []
, table "hdb_catalog" "hdb_table"
[ objectRel $$(nonEmptyText "detail") $
manualConfig "information_schema" "tables" tableNameMapping
, objectRel $$(nonEmptyText "primary_key") $
manualConfig "hdb_catalog" "hdb_primary_key" tableNameMapping
, arrayRel $$(nonEmptyText "columns") $
manualConfig "information_schema" "columns" tableNameMapping
, arrayRel $$(nonEmptyText "foreign_key_constraints") $
manualConfig "hdb_catalog" "hdb_foreign_key_constraint" tableNameMapping
, arrayRel $$(nonEmptyText "relationships") $
manualConfig "hdb_catalog" "hdb_relationship" tableNameMapping
, arrayRel $$(nonEmptyText "permissions") $
manualConfig "hdb_catalog" "hdb_permission_agg" tableNameMapping
, arrayRel $$(nonEmptyText "computed_fields") $
manualConfig "hdb_catalog" "hdb_computed_field" tableNameMapping
, arrayRel $$(nonEmptyText "check_constraints") $
manualConfig "hdb_catalog" "hdb_check_constraint" tableNameMapping
, arrayRel $$(nonEmptyText "unique_constraints") $
manualConfig "hdb_catalog" "hdb_unique_constraint" tableNameMapping
]
, table "hdb_catalog" "hdb_primary_key" []
, table "hdb_catalog" "hdb_foreign_key_constraint" []
, table "hdb_catalog" "hdb_relationship" []
, table "hdb_catalog" "hdb_permission_agg" []
, table "hdb_catalog" "hdb_computed_field" []
, table "hdb_catalog" "hdb_check_constraint" []
, table "hdb_catalog" "hdb_unique_constraint" []
, table "hdb_catalog" "hdb_remote_relationship" []
, table "hdb_catalog" "event_triggers"
[ arrayRel $$(nonEmptyText "events") $
manualConfig "hdb_catalog" "event_log" [("name", "trigger_name")] ]
, table "hdb_catalog" "event_log"
[ objectRel $$(nonEmptyText "trigger") $
manualConfig "hdb_catalog" "event_triggers" [("trigger_name", "name")]
, arrayRel $$(nonEmptyText "logs") $ RUFKeyOn $
ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "event_invocation_logs") "event_id" ]
, table "hdb_catalog" "event_invocation_logs"
[ objectRel $$(nonEmptyText "event") $ RUFKeyOn $ SameTable "event_id" ]
, table "hdb_catalog" "hdb_function" []
, table "hdb_catalog" "hdb_function_agg"
[ objectRel $$(nonEmptyText "return_table_info") $ manualConfig "hdb_catalog" "hdb_table"
[ ("return_type_schema", "table_schema")
, ("return_type_name", "table_name") ] ]
, table "hdb_catalog" "remote_schemas" []
, table "hdb_catalog" "hdb_version" []
, table "hdb_catalog" "hdb_query_collection" []
, table "hdb_catalog" "hdb_allowlist" []
, table "hdb_catalog" "hdb_custom_types" []
, table "hdb_catalog" "hdb_action_permission" []
, table "hdb_catalog" "hdb_action"
[ arrayRel $$(nonEmptyText "permissions") $ manualConfig "hdb_catalog" "hdb_action_permission"
[("action_name", "action_name")]
]
, table "hdb_catalog" "hdb_action_log" []
, table "hdb_catalog" "hdb_role"
[ arrayRel $$(nonEmptyText "action_permissions") $ manualConfig "hdb_catalog" "hdb_action_permission"
[("role_name", "role_name")]
, arrayRel $$(nonEmptyText "permissions") $ manualConfig "hdb_catalog" "hdb_permission_agg"
[("role_name", "role_name")]
]
, table "hdb_catalog" "hdb_cron_triggers"
[ arrayRel $$(nonEmptyText "cron_events") $ RUFKeyOn $
ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "hdb_cron_events") "trigger_name"
]
, table "hdb_catalog" "hdb_cron_events"
[ objectRel $$(nonEmptyText "cron_trigger") $ RUFKeyOn $ SameTable "trigger_name"
, arrayRel $$(nonEmptyText "cron_event_logs") $ RUFKeyOn $
ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "hdb_cron_event_invocation_logs") "event_id"
]
, table "hdb_catalog" "hdb_cron_event_invocation_logs"
[ objectRel $$(nonEmptyText "cron_event") $ RUFKeyOn $ SameTable "event_id"
]
, table "hdb_catalog" "hdb_scheduled_events"
[ arrayRel $$(nonEmptyText "scheduled_event_logs") $ RUFKeyOn $
ArrRelUsingFKeyOn (QualifiedObject "hdb_catalog" "hdb_scheduled_event_invocation_logs") "event_id"
]
, table "hdb_catalog" "hdb_scheduled_event_invocation_logs"
[ objectRel $$(nonEmptyText "scheduled_event") $ RUFKeyOn $ SameTable "event_id"
]
]
tableNameMapping =
[ ("table_schema", "table_schema")
, ("table_name", "table_name") ]
table schemaName tableName relationships = (QualifiedObject schemaName tableName, relationships)
objectRel name using = Left $ RelDef (RelName name) using Nothing
arrayRel name using = Right $ RelDef (RelName name) using Nothing
manualConfig schemaName tableName columns =
RUManual $ RelManualConfig (QualifiedObject schemaName tableName) (HM.fromList columns) Nothing