graphql-engine/server/src-lib/Hasura/RQL/DDL/Schema/Rename.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

471 lines
18 KiB
Haskell

-- | Functions for updating the metadata (with integrity checking) to incorporate schema changes
-- discovered after applying a user-supplied SQL query. None of these functions modify the schema
-- cache, so it must be reloaded after the metadata is updated.
module Hasura.RQL.DDL.Schema.Rename
( renameTableInMetadata
, renameColumnInMetadata
, renameRelationshipInMetadata
)
where
import Control.Lens.Combinators
import Control.Lens.Operators
-- import Hasura.Backends.Postgres.SQL.Types
import Hasura.Prelude
import Hasura.RQL.DDL.Permission
import Hasura.RQL.Types
import Hasura.Session
import Data.Aeson
import Data.Text.Extended
import Data.Typeable (cast)
import qualified Data.HashMap.Strict as M
import qualified Data.HashMap.Strict.InsOrd as OMap
import qualified Data.HashSet as Set
import qualified Data.List.NonEmpty as NE
import qualified Language.GraphQL.Draft.Syntax as G
data RenameItem (b :: BackendType) a
= RenameItem
{ _riTable :: !(TableName b)
, _riOld :: !a
, _riNew :: !a
}
type RenameCol (b :: BackendType) = RenameItem b (Column b)
data RenameField b
= RFCol !(RenameCol b)
| RFRel !(RenameItem b RelName)
type RenameTable b = (TableName b, TableName b)
data Rename b
= RTable !(RenameTable b)
| RField !(RenameField b)
otherDeps :: QErrM m => Text -> SchemaObjId -> m ()
otherDeps errMsg d =
throw500 $ "unexpected dependancy "
<> reportSchemaObj d <> "; " <> errMsg
renameTableInMetadata
:: ( MonadError QErr m
, CacheRM m
, MonadWriter MetadataModifier m
, BackendMetadata b
)
=> SourceName -> TableName b -> TableName b -> m ()
renameTableInMetadata source newQT oldQT = do
sc <- askSchemaCache
let allDeps = getDependentObjs sc $ SOSourceObj source $ SOITable oldQT
-- update all dependant schema objects
forM_ allDeps $ \case
(SOSourceObj _ (SOITableObj refQT (TORel rn))) ->
onJust (cast refQT) \tn -> updateRelDefs source tn rn (oldQT, newQT)
(SOSourceObj _ (SOITableObj refQT (TOPerm rn pt))) ->
onJust (cast refQT) \tn -> updatePermFlds source tn rn pt $ RTable (oldQT, newQT)
-- A trigger's definition is not dependent on the table directly
(SOSourceObj _ (SOITableObj _ (TOTrigger _))) -> pure ()
-- A remote relationship's definition is not dependent on the table directly
(SOSourceObj _ (SOITableObj _ (TORemoteRel _))) -> pure ()
d -> otherDeps errMsg d
-- Update table name in metadata
tell $ MetadataModifier $ metaSources.ix source.toSourceMetadata.smTables %~ \tables ->
flip (maybe tables) (OMap.lookup oldQT tables) $
\tableMeta -> OMap.delete oldQT $ OMap.insert newQT tableMeta{_tmTable = newQT} tables
where
errMsg = "cannot rename table " <> oldQT <<> " to " <>> newQT
renameColumnInMetadata
:: forall b m
. ( MonadError QErr m
, CacheRM m
, MonadWriter MetadataModifier m
, BackendMetadata b
)
=> Column b -> Column b -> SourceName -> TableName b -> FieldInfoMap (FieldInfo b) -> m ()
renameColumnInMetadata oCol nCol source qt fieldInfo = do
sc <- askSchemaCache
-- Check if any relation exists with new column name
assertFldNotExists
-- Fetch dependent objects
let depObjs = getDependentObjs sc $ SOSourceObj source $
SOITableObj qt $ TOCol oCol
renameFld = RFCol $ RenameItem qt oCol nCol
-- Update dependent objects
forM_ depObjs $ \case
(SOSourceObj _ (SOITableObj refQT (TOPerm role pt))) ->
onJust (cast refQT) \tn -> updatePermFlds source tn role pt $ RField renameFld
(SOSourceObj _ (SOITableObj refQT (TORel rn))) ->
onJust (cast refQT) \tn -> updateColInRel source tn rn $ RenameItem qt oCol nCol
(SOSourceObj _ (SOITableObj refQT (TOTrigger triggerName))) ->
onJust (cast refQT) \tn -> tell $ MetadataModifier $
tableMetadataSetter source tn.tmEventTriggers.ix triggerName %~ (updateColumnInEventTrigger tn oCol nCol qt)
(SOSourceObj _ (SOITableObj _ (TORemoteRel remoteRelName))) ->
updateColInRemoteRelationship source remoteRelName $ RenameItem qt oCol nCol
d -> otherDeps errMsg d
-- Update custom column names
possiblyUpdateCustomColumnNames source qt oCol nCol
where
errMsg = "cannot rename column " <> oCol <<> " to " <>> nCol
assertFldNotExists =
case M.lookup (fromCol @b oCol) fieldInfo of
Just (FIRelationship _) ->
throw400 AlreadyExists $ "cannot rename column " <> oCol
<<> " to " <> nCol <<> " in table " <> qt <<>
" as a relationship with the name already exists"
_ -> pure ()
renameRelationshipInMetadata
:: ( MonadError QErr m
, CacheRM m
, MonadWriter MetadataModifier m
, BackendMetadata b
)
=> SourceName -> TableName b -> RelName -> RelType -> RelName -> m ()
renameRelationshipInMetadata source qt oldRN relType newRN = do
sc <- askSchemaCache
let depObjs = getDependentObjs sc $ SOSourceObj source $
SOITableObj qt $ TORel oldRN
renameFld = RFRel $ RenameItem qt oldRN newRN
forM_ depObjs $ \case
(SOSourceObj _ (SOITableObj refQT (TOPerm role pt))) ->
onJust (cast refQT) \tn -> updatePermFlds source tn role pt $ RField renameFld
d -> otherDeps errMsg d
tell $ MetadataModifier $ tableMetadataSetter source qt %~ case relType of
ObjRel -> tmObjectRelationships %~ rewriteRelationships
ArrRel -> tmArrayRelationships %~ rewriteRelationships
where
errMsg = "cannot rename relationship " <> oldRN <<> " to " <>> newRN
rewriteRelationships
:: Relationships (RelDef a) -> Relationships (RelDef a)
rewriteRelationships relationsMap =
flip (maybe relationsMap) (OMap.lookup oldRN relationsMap) $
\rd -> OMap.insert newRN rd{_rdName = newRN} $ OMap.delete oldRN relationsMap
-- update table names in relationship definition
updateRelDefs
:: forall b m
. ( MonadError QErr m
, CacheRM m
, MonadWriter MetadataModifier m
, BackendMetadata b
)
=> SourceName -> TableName b -> RelName -> RenameTable b -> m ()
updateRelDefs source qt rn renameTable = do
fim <- askFieldInfoMap source qt
ri <- askRelType fim rn ""
tell $ MetadataModifier $ tableMetadataSetter source qt %~ case riType ri of
ObjRel -> tmObjectRelationships.ix rn %~ updateObjRelDef renameTable
ArrRel -> tmArrayRelationships.ix rn %~ updateArrRelDef renameTable
where
updateObjRelDef :: RenameTable b -> ObjRelDef b -> ObjRelDef b
updateObjRelDef (oldQT, newQT) =
rdUsing %~ \case
RUFKeyOn fk -> RUFKeyOn fk
RUManual (RelManualConfig origQT rmCols rmIO) ->
let updQT = bool origQT newQT $ oldQT == origQT
in RUManual $ RelManualConfig updQT rmCols rmIO
updateArrRelDef :: RenameTable b -> ArrRelDef b -> ArrRelDef b
updateArrRelDef (oldQT, newQT) =
rdUsing %~ \case
RUFKeyOn (ArrRelUsingFKeyOn origQT c) ->
let updQT = getUpdQT origQT
in RUFKeyOn $ ArrRelUsingFKeyOn updQT c
RUManual (RelManualConfig origQT rmCols rmIO) ->
let updQT = getUpdQT origQT
in RUManual $ RelManualConfig updQT rmCols rmIO
where
getUpdQT origQT = bool origQT newQT $ oldQT == origQT
-- | update fields in permissions
updatePermFlds
:: forall b m
. ( MonadError QErr m
, CacheRM m
, MonadWriter MetadataModifier m
, BackendMetadata b
)
=> SourceName -> TableName b -> RoleName -> PermType -> Rename b -> m ()
updatePermFlds source refQT rn pt rename = do
tables <- askTableCache source
let withTables :: Reader (TableCache b) a -> a
withTables = flip runReader tables
tell $ MetadataModifier $
tableMetadataSetter source refQT %~ case pt of
PTInsert ->
tmInsertPermissions.ix rn.pdPermission %~ \insPerm ->
withTables $ updateInsPermFlds refQT rename insPerm
PTSelect ->
tmSelectPermissions.ix rn.pdPermission %~ \selPerm ->
withTables $ updateSelPermFlds refQT rename selPerm
PTUpdate ->
tmUpdatePermissions.ix rn.pdPermission %~ \updPerm ->
withTables $ updateUpdPermFlds refQT rename updPerm
PTDelete ->
tmDeletePermissions.ix rn.pdPermission %~ \delPerm ->
withTables $ updateDelPermFlds refQT rename delPerm
updateInsPermFlds
:: (MonadReader (TableCache b) m, Backend b)
=> TableName b -> Rename b -> InsPerm b -> m (InsPerm b)
updateInsPermFlds refQT rename (InsPerm chk preset cols mBackendOnly) =
case rename of
RTable rt -> do
let updChk = updateTableInBoolExp rt chk
pure $ InsPerm updChk preset cols mBackendOnly
RField rf -> do
updChk <- updateFieldInBoolExp refQT rf chk
let updPresetM = updatePreset refQT rf <$> preset
updColsM = updateCols refQT rf <$> cols
pure $ InsPerm updChk updPresetM updColsM mBackendOnly
updateSelPermFlds
:: (MonadReader (TableCache b) m, Backend b)
=> TableName b -> Rename b -> SelPerm b -> m (SelPerm b)
updateSelPermFlds refQT rename (SelPerm cols fltr limit aggAllwd computedFields) = do
case rename of
RTable rt -> do
let updFltr = updateTableInBoolExp rt fltr
pure $ SelPerm cols updFltr limit aggAllwd computedFields
RField rf -> do
updFltr <- updateFieldInBoolExp refQT rf fltr
let updCols = updateCols refQT rf cols
pure $ SelPerm updCols updFltr limit aggAllwd computedFields
updateUpdPermFlds
:: (MonadReader (TableCache b) m, Backend b)
=> TableName b -> Rename b -> UpdPerm b -> m (UpdPerm b)
updateUpdPermFlds refQT rename (UpdPerm cols preset fltr check) = do
case rename of
RTable rt -> do
let updFltr = updateTableInBoolExp rt fltr
updCheck = fmap (updateTableInBoolExp rt) check
pure $ UpdPerm cols preset updFltr updCheck
RField rf -> do
updFltr <- updateFieldInBoolExp refQT rf fltr
updCheck <- traverse (updateFieldInBoolExp refQT rf) check
let updCols = updateCols refQT rf cols
updPresetM = updatePreset refQT rf <$> preset
pure $ UpdPerm updCols updPresetM updFltr updCheck
updateDelPermFlds
:: (MonadReader (TableCache b) m, Backend b)
=> TableName b -> Rename b -> DelPerm b -> m (DelPerm b)
updateDelPermFlds refQT rename (DelPerm fltr) = do
DelPerm <$> case rename of
RTable rt -> pure $ updateTableInBoolExp rt fltr
RField rf -> updateFieldInBoolExp refQT rf fltr
updatePreset
:: (Backend b)
=> TableName b -> RenameField b -> (ColumnValues b Value) -> (ColumnValues b Value)
updatePreset qt rf obj =
case rf of
RFCol (RenameItem opQT oCol nCol) ->
if qt == opQT then updatePreset' oCol nCol
else obj
_ -> obj
where
updatePreset' oCol nCol =
M.fromList updItems
where
updItems= map procObjItem $ M.toList obj
procObjItem (pgCol, v) =
let isUpdated = pgCol == oCol
updCol = bool pgCol nCol isUpdated
in (updCol, v)
updateCols
:: (Backend b) => TableName b -> RenameField b -> PermColSpec b -> PermColSpec b
updateCols qt rf permSpec =
case rf of
RFCol (RenameItem opQT oCol nCol) ->
if qt == opQT then updateCols' oCol nCol permSpec
else permSpec
_ -> permSpec
where
updateCols' oCol nCol cols = case cols of
PCStar -> cols
PCCols c -> PCCols $ flip map c $
\col -> if col == oCol then nCol else col
updateTableInBoolExp :: (Backend b) => RenameTable b -> BoolExp b -> BoolExp b
updateTableInBoolExp (oldQT, newQT) =
over _Wrapped . transform $ (_BoolExists . geTable) %~ \rqfQT ->
if rqfQT == oldQT then newQT else rqfQT
updateFieldInBoolExp
:: (MonadReader (TableCache b) m, Backend b)
=> TableName b -> RenameField b -> BoolExp b -> m (BoolExp b)
updateFieldInBoolExp qt rf be = BoolExp <$>
case unBoolExp be of
BoolAnd exps -> BoolAnd <$> procExps exps
BoolOr exps -> BoolOr <$> procExps exps
BoolNot e -> BoolNot <$> updateBoolExp' e
BoolExists (GExists refqt wh) ->
(BoolExists . GExists refqt . unBoolExp)
<$> updateFieldInBoolExp refqt rf (BoolExp wh)
BoolFld fld -> BoolFld <$> updateColExp qt rf fld
where
procExps = mapM updateBoolExp'
updateBoolExp' =
fmap unBoolExp . updateFieldInBoolExp qt rf . BoolExp
updateColExp
:: forall b m. (MonadReader (TableCache b) m, Backend b)
=> TableName b -> RenameField b -> ColExp -> m ColExp
updateColExp qt rf (ColExp fld val) =
ColExp updatedFld <$> updatedVal
where
updatedFld = bool fld nFld $ opQT == qt && oFld == fld
updatedVal = do
tables <- ask
let maybeFieldInfo = M.lookup qt tables >>=
M.lookup fld . _tciFieldInfoMap . _tiCoreInfo
case maybeFieldInfo of
Nothing -> pure val
Just fi -> case fi of
FIColumn _ -> pure val
FIComputedField _ -> pure val
FIRelationship ri -> do
let remTable = riRTable ri
case decodeValue val of
Left _ -> pure val
Right be -> toJSON <$> updateFieldInBoolExp remTable rf be
FIRemoteRelationship{} -> pure val
(oFld, nFld, opQT) = case rf of
RFCol (RenameItem tn oCol nCol) -> (fromCol @b oCol, fromCol @b nCol, tn)
RFRel (RenameItem tn oRel nRel) -> (fromRel oRel, fromRel nRel, tn)
-- rename columns in relationship definitions
updateColInRel
:: (CacheRM m, MonadWriter MetadataModifier m, BackendMetadata b)
=> SourceName -> TableName b -> RelName -> RenameCol b -> m ()
updateColInRel source fromQT rn rnCol = do
tables <- askSourceTables source
let maybeRelInfo =
tables ^? ix fromQT.tiCoreInfo.tciFieldInfoMap.ix (fromRel rn)._FIRelationship
forM_ maybeRelInfo $ \relInfo ->
tell $ MetadataModifier $ tableMetadataSetter source fromQT %~
case riType relInfo of
ObjRel -> tmObjectRelationships.ix rn.rdUsing %~
updateColInObjRel fromQT (riRTable relInfo) rnCol
ArrRel -> tmArrayRelationships.ix rn.rdUsing %~
updateColInArrRel fromQT (riRTable relInfo) rnCol
updateColInRemoteRelationship
:: forall b m
. ( MonadError QErr m
, MonadWriter MetadataModifier m
, BackendMetadata b
)
=> SourceName -> RemoteRelationshipName -> RenameCol b -> m ()
updateColInRemoteRelationship source remoteRelationshipName renameCol = do
oldColName <- parseGraphQLName $ toTxt oldCol
newColName <- parseGraphQLName $ toTxt newCol
tell $ MetadataModifier $
tableMetadataSetter source qt.tmRemoteRelationships.ix remoteRelationshipName.rrmDefinition %~
(rrdHasuraFields %~ modifyHasuraFields) .
(rrdRemoteField %~ modifyFieldCalls oldColName newColName)
where
(RenameItem qt oldCol newCol) = renameCol
modifyHasuraFields = Set.insert (fromCol @b newCol) . Set.delete (fromCol @b oldCol)
modifyFieldCalls oldColName newColName =
RemoteFields
. NE.map (\(FieldCall name args) ->
let remoteArgs = getRemoteArguments args
in FieldCall name $ RemoteArguments $
fmap (replaceVariableName oldColName newColName) remoteArgs
)
. unRemoteFields
parseGraphQLName txt = onNothing (G.mkName txt) $ throw400 ParseFailed errMsg
where
errMsg = txt <> " is not a valid GraphQL name"
replaceVariableName :: G.Name -> G.Name -> G.Value G.Name -> G.Value G.Name
replaceVariableName oldColName newColName = \case
G.VVariable oldColName' ->
G.VVariable $ bool oldColName newColName $ oldColName == oldColName'
G.VList values -> G.VList $ map (replaceVariableName oldColName newColName) values
G.VObject values ->
G.VObject $ fmap (replaceVariableName oldColName newColName) values
v -> v
updateColInObjRel
:: (Backend b)
=> TableName b -> TableName b -> RenameCol b -> ObjRelUsing b -> ObjRelUsing b
updateColInObjRel fromQT toQT rnCol = \case
RUFKeyOn c ->
RUFKeyOn $ updateRelChoice fromQT toQT rnCol c
RUManual manConfig ->
RUManual $ updateRelManualConfig fromQT toQT rnCol manConfig
updateRelChoice
:: Backend b
=> TableName b
-> TableName b
-> RenameCol b
-> ObjRelUsingChoice b
-> ObjRelUsingChoice b
updateRelChoice fromQT toQT rnCol =
\case
SameTable col -> SameTable $ getNewCol rnCol fromQT col
RemoteTable t c -> RemoteTable t (getNewCol rnCol toQT c)
updateColInArrRel
:: (Backend b)
=> TableName b -> TableName b -> RenameCol b -> ArrRelUsing b -> ArrRelUsing b
updateColInArrRel fromQT toQT rnCol = \case
RUFKeyOn (ArrRelUsingFKeyOn t c) ->
let updCol = getNewCol rnCol toQT c
in RUFKeyOn $ ArrRelUsingFKeyOn t updCol
RUManual manConfig -> RUManual $ updateRelManualConfig fromQT toQT rnCol manConfig
type ColMap b = HashMap (Column b) (Column b)
getNewCol
:: (Backend b) => RenameCol b -> TableName b -> Column b -> Column b
getNewCol rnCol qt col =
if opQT == qt && col == oCol then nCol else col
where
RenameItem opQT oCol nCol = rnCol
updateRelManualConfig
:: (Backend b)
=> TableName b -> TableName b -> RenameCol b -> RelManualConfig b -> RelManualConfig b
updateRelManualConfig fromQT toQT rnCol manConfig =
RelManualConfig tn (updateColMap fromQT toQT rnCol colMap) io
where
RelManualConfig tn colMap io = manConfig
updateColMap
:: (Backend b)
=> TableName b -> TableName b -> RenameCol b -> ColMap b -> ColMap b
updateColMap fromQT toQT rnCol =
M.fromList . map (modCol fromQT *** modCol toQT) . M.toList
where
RenameItem qt oCol nCol = rnCol
modCol colQt col = if colQt == qt && col == oCol then nCol else col
possiblyUpdateCustomColumnNames
:: (MonadWriter MetadataModifier m, BackendMetadata b)
=> SourceName -> TableName b -> Column b -> Column b -> m ()
possiblyUpdateCustomColumnNames source qt oCol nCol = do
let updateCustomColumns customColumns =
M.fromList $ flip map (M.toList customColumns) $
\(dbCol, val) -> (, val) $ if dbCol == oCol then nCol else dbCol
tell $ MetadataModifier $
tableMetadataSetter source qt.tmConfiguration.tcCustomColumnNames %~ updateCustomColumns