2021-08-09 13:20:04 +03:00
|
|
|
{-# LANGUAGE Arrows #-}
|
2019-12-09 01:17:39 +03:00
|
|
|
|
|
|
|
module Hasura.RQL.DDL.Schema.Cache.Permission
|
2021-09-24 01:56:37 +03:00
|
|
|
( buildTablePermissions,
|
|
|
|
mkPermissionMetadataObject,
|
|
|
|
mkRemoteSchemaPermissionMetadataObject,
|
|
|
|
orderRoles,
|
|
|
|
OrderedRoles,
|
|
|
|
_unOrderedRoles,
|
|
|
|
mkBooleanPermissionMap,
|
|
|
|
resolveCheckPermission,
|
|
|
|
)
|
|
|
|
where
|
2019-12-09 01:17:39 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
import Control.Arrow.Extended
|
2022-04-06 15:47:35 +03:00
|
|
|
import Control.Arrow.Interpret
|
2021-09-24 01:56:37 +03:00
|
|
|
import Data.Aeson
|
|
|
|
import Data.Graph qualified as G
|
|
|
|
import Data.HashMap.Strict qualified as M
|
|
|
|
import Data.Proxy
|
|
|
|
import Data.Sequence qualified as Seq
|
|
|
|
import Data.Text.Extended
|
|
|
|
import Hasura.Base.Error
|
|
|
|
import Hasura.Incremental qualified as Inc
|
|
|
|
import Hasura.Prelude
|
|
|
|
import Hasura.RQL.DDL.Permission
|
|
|
|
import Hasura.RQL.DDL.Schema.Cache.Common
|
|
|
|
import Hasura.RQL.Types
|
|
|
|
import Hasura.RQL.Types.Roles.Internal
|
|
|
|
( CheckPermission (..),
|
|
|
|
CombineRolePermInfo (..),
|
|
|
|
rolePermInfoToCombineRolePermInfo,
|
|
|
|
)
|
|
|
|
import Hasura.SQL.AnyBackend qualified as AB
|
|
|
|
import Hasura.Session
|
2021-04-22 00:44:37 +03:00
|
|
|
|
2021-08-09 13:20:04 +03:00
|
|
|
{- Note: [Inherited roles architecture for read queries]
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
[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 14:14:13 +03:00
|
|
|
|
|
|
|
1. Schema generation
|
|
|
|
--------------------
|
|
|
|
|
|
|
|
Schema generation for inherited roles is similar to the schema
|
|
|
|
generation of non-inherited roles. In the case of inherited roles,
|
2021-08-09 13:20:04 +03:00
|
|
|
we combine the `SelectPermInfo`s of the
|
[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 14:14:13 +03:00
|
|
|
inherited role's role set and a new `SelectPermInfo` will be generated
|
|
|
|
which will be the select permission of the inherited role.
|
|
|
|
|
2021-08-09 13:20:04 +03:00
|
|
|
Two `SelPermInfo`s will be combined in the following manner:
|
|
|
|
|
|
|
|
1. Columns - The `SelPermInfo` contains a hashset of the columns that are
|
|
|
|
accessible to the role. To combine two `SelPermInfo`s, every column of the
|
|
|
|
hashset is coupled with the boolean expression (filter) of the `SelPermInfo`
|
|
|
|
and a hash map of all the columns is created out of it, this hashmap is
|
|
|
|
generated for the `SelPermInfo`s that are going to be combined. These hashmaps
|
|
|
|
are then unioned and the values of these hashmaps are `OR`ed. When a column
|
|
|
|
is accessible to all the select permissions then the nullability of the column
|
|
|
|
is inferred from the DB column otherwise the column is explicitly marked as
|
|
|
|
nullable to accomodate cell-value nullification.
|
|
|
|
2. Scalar computed fields - Scalar computed fields work the same as Columns (#1)
|
|
|
|
3. Filter / Boolean expression - The filters are combined using a `BoolOr`
|
|
|
|
4. Limit - Limits are combined by taking the maximum of the two limits
|
|
|
|
5. Allow Aggregation - Aggregation is allowed, if any of the permissions allow it.
|
|
|
|
6. Request Headers - Request headers are concatenated
|
|
|
|
|
[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 14:14:13 +03:00
|
|
|
2. SQL generation
|
|
|
|
-----------------
|
|
|
|
|
|
|
|
See note [SQL generation for inherited roles]
|
|
|
|
|
|
|
|
3. Introspection
|
|
|
|
----------------
|
|
|
|
|
|
|
|
The columns accessible to an inherited role are explicitly set to
|
|
|
|
nullable irrespective of the nullability of the DB column to accomodate
|
|
|
|
cell value nullification.
|
|
|
|
-}
|
|
|
|
|
2021-08-09 13:20:04 +03:00
|
|
|
mkBooleanPermissionMap :: (RoleName -> a) -> HashMap RoleName a -> OrderedRoles -> HashMap RoleName a
|
|
|
|
mkBooleanPermissionMap constructorFn metadataPermissions orderedRoles =
|
|
|
|
foldl' combineBooleanPermission metadataPermissions $ _unOrderedRoles orderedRoles
|
[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 14:14:13 +03:00
|
|
|
where
|
2021-08-09 13:20:04 +03:00
|
|
|
combineBooleanPermission accumulatedPermMap (Role roleName (ParentRoles parentRoles)) =
|
|
|
|
case M.lookup roleName accumulatedPermMap of
|
|
|
|
-- We check if a permission for the given role exists in the metadata, if it
|
|
|
|
-- exists, we use that
|
2021-09-24 01:56:37 +03:00
|
|
|
Just _ -> accumulatedPermMap
|
2021-08-09 13:20:04 +03:00
|
|
|
-- 2. When the permission doesn't exist, we try to inherit the permission from its parent roles
|
|
|
|
-- For boolean permissions, if any of the parent roles have a permission to access an entity,
|
|
|
|
-- then the inherited role will also be able to access the entity.
|
|
|
|
Nothing ->
|
|
|
|
-- see Note [Roles Inheritance]
|
|
|
|
let canInheritPermission = any ((`M.member` accumulatedPermMap)) (toList parentRoles)
|
2021-09-24 01:56:37 +03:00
|
|
|
in if canInheritPermission
|
|
|
|
then M.insert roleName (constructorFn roleName) accumulatedPermMap
|
|
|
|
else accumulatedPermMap
|
[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 14:14:13 +03:00
|
|
|
|
2021-08-09 13:20:04 +03:00
|
|
|
-- | `OrderedRoles` is a data type to hold topologically sorted roles
|
|
|
|
-- according to each role's parent roles, see `orderRoles` for more details.
|
2021-09-24 01:56:37 +03:00
|
|
|
newtype OrderedRoles = OrderedRoles {_unOrderedRoles :: [Role]}
|
2021-08-09 13:20:04 +03:00
|
|
|
deriving (Eq, Generic)
|
2021-09-24 01:56:37 +03:00
|
|
|
|
2021-08-09 13:20:04 +03:00
|
|
|
instance Inc.Cacheable OrderedRoles
|
[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 14:14:13 +03:00
|
|
|
|
2021-07-17 00:18:58 +03:00
|
|
|
-- | 'orderRoles' is used to order the roles, in such a way that given
|
|
|
|
-- a role R with n parent roles - PR1, PR2 .. PRn, then the 'orderRoles'
|
|
|
|
-- function will order the roles in such a way that all the parent roles
|
|
|
|
-- precede the role R. Note that the order of the parent roles itself doesn't
|
|
|
|
-- matter as long as they precede the roles on which they are dependent on.
|
|
|
|
--
|
|
|
|
-- For example, the orderRoles may return `[PR1, PR3, PR2, ... PRn, R]`
|
|
|
|
-- or `[PR5, PR3, PR1 ... R]`, both of them are correct because all
|
|
|
|
-- the parent roles precede the inherited role R, assuming the parent roles
|
|
|
|
-- themselves don't have any parents for the sake of this example.
|
2021-09-24 01:56:37 +03:00
|
|
|
orderRoles ::
|
|
|
|
MonadError QErr m =>
|
|
|
|
[Role] ->
|
|
|
|
m OrderedRoles
|
2021-07-17 00:18:58 +03:00
|
|
|
orderRoles allRoles = do
|
|
|
|
-- inherited roles can be created from other inherited and non-inherited roles
|
|
|
|
-- So, roles can be thought of as a graph where non-inherited roles don't have
|
|
|
|
-- any outgoing edges and inherited roles as nodes with edges to its parent roles
|
|
|
|
-- However, we can't allow cyclic roles since permissions built by a role is used
|
|
|
|
-- by the dependent roles to build their permissions and if cyclic roles were to be
|
|
|
|
-- allowed, the permissions building will be stuck in an infinite loop
|
2021-09-24 01:56:37 +03:00
|
|
|
let graphNodesList = [(role, _rRoleName role, toList (_unParentRoles . _rParentRoles $ role)) | role <- allRoles]
|
2021-07-17 00:18:58 +03:00
|
|
|
let orderedGraphNodes = G.stronglyConnComp graphNodesList -- topologically sort the nodes of the graph
|
|
|
|
cyclicRoles = filter checkCycle orderedGraphNodes
|
|
|
|
unless (null cyclicRoles) $ do
|
|
|
|
-- we're appending the first element of the list at the end, so that the error message will
|
|
|
|
-- contain the complete cycle of the roles
|
2021-09-24 01:56:37 +03:00
|
|
|
let roleCycles = map (tshow . map (roleNameToTxt . _rRoleName) . appendFirstElementAtEnd . G.flattenSCC) cyclicRoles
|
2021-07-17 00:18:58 +03:00
|
|
|
throw400 CyclicDependency $ "found cycle(s) in roles: " <> commaSeparated roleCycles
|
|
|
|
let allOrderedRoles = G.flattenSCCs orderedGraphNodes
|
2021-08-09 13:20:04 +03:00
|
|
|
pure $ OrderedRoles allOrderedRoles
|
2021-07-17 00:18:58 +03:00
|
|
|
where
|
|
|
|
checkCycle = \case
|
|
|
|
G.AcyclicSCC _ -> False
|
2021-09-24 01:56:37 +03:00
|
|
|
G.CyclicSCC _ -> True
|
2021-07-17 00:18:58 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
appendFirstElementAtEnd [] = []
|
|
|
|
appendFirstElementAtEnd (x : xs) = (x : xs) ++ [x]
|
2021-07-17 00:18:58 +03:00
|
|
|
|
2022-04-06 15:47:35 +03:00
|
|
|
-- | `resolveCheckPermission` is a helper function which will convert the indermediate
|
2021-08-09 13:20:04 +03:00
|
|
|
-- type `CheckPermission` to its original type. It will record any metadata inconsistencies, if exists.
|
2021-09-24 01:56:37 +03:00
|
|
|
resolveCheckPermission ::
|
2022-04-06 15:47:35 +03:00
|
|
|
forall m p.
|
|
|
|
(MonadWriter (Seq CollectedInfo) m) =>
|
|
|
|
CheckPermission p ->
|
|
|
|
RoleName ->
|
|
|
|
InconsistentRoleEntity ->
|
|
|
|
m (Maybe p)
|
|
|
|
resolveCheckPermission checkPermission roleName inconsistentEntity = do
|
2021-08-09 13:20:04 +03:00
|
|
|
case checkPermission of
|
|
|
|
CPInconsistent -> do
|
|
|
|
let inconsistentObj =
|
|
|
|
-- check `Conflicts while inheriting permissions` in `rfcs/inherited-roles-improvements.md`
|
|
|
|
CIInconsistency $
|
2021-09-24 01:56:37 +03:00
|
|
|
ConflictingInheritedPermission roleName inconsistentEntity
|
2022-04-06 15:47:35 +03:00
|
|
|
tell $ Seq.singleton inconsistentObj
|
|
|
|
pure Nothing
|
|
|
|
CPDefined permissionDefn -> pure $ Just permissionDefn
|
|
|
|
CPUndefined -> pure Nothing
|
2021-08-09 13:20:04 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
resolveCheckTablePermission ::
|
2022-04-06 15:47:35 +03:00
|
|
|
forall b perm m.
|
|
|
|
( MonadWriter (Seq CollectedInfo) m,
|
2021-09-24 01:56:37 +03:00
|
|
|
BackendMetadata b
|
|
|
|
) =>
|
2022-04-06 15:47:35 +03:00
|
|
|
CheckPermission perm ->
|
|
|
|
Maybe (RolePermInfo b) ->
|
|
|
|
(RolePermInfo b -> Maybe perm) ->
|
|
|
|
RoleName ->
|
|
|
|
SourceName ->
|
|
|
|
TableName b ->
|
|
|
|
PermType ->
|
|
|
|
m (Maybe perm)
|
|
|
|
resolveCheckTablePermission inheritedRolePermission accumulatedRolePermInfo permAcc roleName source table permType = do
|
2021-08-09 13:20:04 +03:00
|
|
|
-- when for a given entity and role, a permission exists in the metadata, we override the metadata permission
|
|
|
|
-- over the inherited permission
|
2022-04-06 15:47:35 +03:00
|
|
|
let checkPermission = maybe inheritedRolePermission CPDefined (permAcc =<< accumulatedRolePermInfo)
|
2021-08-09 13:20:04 +03:00
|
|
|
inconsistentRoleEntity = InconsistentTablePermission source (toTxt table) permType
|
2022-04-06 15:47:35 +03:00
|
|
|
resolveCheckPermission checkPermission roleName inconsistentRoleEntity
|
2021-08-09 13:20:04 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
buildTablePermissions ::
|
2022-04-06 15:47:35 +03:00
|
|
|
forall b m arr.
|
2021-09-24 01:56:37 +03:00
|
|
|
( ArrowChoice arr,
|
|
|
|
Inc.ArrowDistribute arr,
|
|
|
|
Inc.ArrowCache m arr,
|
|
|
|
MonadError QErr m,
|
|
|
|
ArrowWriter (Seq CollectedInfo) arr,
|
|
|
|
BackendMetadata b,
|
|
|
|
Inc.Cacheable (Proxy b)
|
|
|
|
) =>
|
|
|
|
( Proxy b,
|
|
|
|
SourceName,
|
|
|
|
Inc.Dependency (TableCoreCache b),
|
|
|
|
FieldInfoMap (FieldInfo b),
|
|
|
|
TablePermissionInputs b,
|
|
|
|
OrderedRoles
|
|
|
|
)
|
|
|
|
`arr` (RolePermInfoMap b)
|
2021-07-17 00:18:58 +03:00
|
|
|
buildTablePermissions = Inc.cache proc (proxy, source, tableCache, tableFields, tablePermissions, orderedRoles) -> do
|
2020-12-08 17:22:31 +03:00
|
|
|
let alignedPermissions = alignPermissions tablePermissions
|
|
|
|
table = _tpiTable tablePermissions
|
2021-07-17 00:18:58 +03:00
|
|
|
metadataRolePermissions <-
|
2021-09-24 01:56:37 +03:00
|
|
|
(|
|
|
|
|
Inc.keyed
|
|
|
|
( \_ (insertPermission, selectPermission, updatePermission, deletePermission) -> do
|
|
|
|
insert <- buildPermission -< (proxy, tableCache, source, table, tableFields, listToMaybe insertPermission)
|
|
|
|
select <- buildPermission -< (proxy, tableCache, source, table, tableFields, listToMaybe selectPermission)
|
|
|
|
update <- buildPermission -< (proxy, tableCache, source, table, tableFields, listToMaybe updatePermission)
|
|
|
|
delete <- buildPermission -< (proxy, tableCache, source, table, tableFields, listToMaybe deletePermission)
|
|
|
|
returnA -< RolePermInfo insert select update delete
|
|
|
|
)
|
|
|
|
|) alignedPermissions
|
2021-10-05 15:28:38 +03:00
|
|
|
(|
|
|
|
|
foldlA'
|
|
|
|
( \accumulatedRolePermMap (Role roleName (ParentRoles parentRoles)) -> do
|
|
|
|
parentRolePermissions <-
|
|
|
|
bindA
|
|
|
|
-< for (toList parentRoles) $ \role ->
|
|
|
|
onNothing (M.lookup role accumulatedRolePermMap) $
|
|
|
|
throw500 $
|
|
|
|
-- this error will ideally never be thrown, but if it's thrown then
|
|
|
|
-- it's possible that the permissions for the role do exist, but it's
|
|
|
|
-- not yet built due to wrong ordering of the roles, check `orderRoles`
|
|
|
|
"buildTablePermissions: table role permissions for role: " <> role <<> " not found"
|
|
|
|
let combinedParentRolePermInfo = mconcat $ fmap rolePermInfoToCombineRolePermInfo parentRolePermissions
|
|
|
|
selectPermissionsCount = length $ filter (isJust . _permSel) parentRolePermissions
|
|
|
|
let accumulatedRolePermission = M.lookup roleName accumulatedRolePermMap
|
|
|
|
let roleSelectPermission =
|
|
|
|
case (_permSel =<< accumulatedRolePermission) of
|
|
|
|
Just metadataSelectPerm -> Just metadataSelectPerm
|
|
|
|
Nothing -> combinedSelPermInfoToSelPermInfo selectPermissionsCount <$> (crpiSelPerm combinedParentRolePermInfo)
|
2022-04-06 15:47:35 +03:00
|
|
|
roleInsertPermission <- interpretWriter -< resolveCheckTablePermission (crpiInsPerm combinedParentRolePermInfo) accumulatedRolePermission _permIns roleName source table PTInsert
|
|
|
|
roleUpdatePermission <- interpretWriter -< resolveCheckTablePermission (crpiUpdPerm combinedParentRolePermInfo) accumulatedRolePermission _permUpd roleName source table PTUpdate
|
|
|
|
roleDeletePermission <- interpretWriter -< resolveCheckTablePermission (crpiDelPerm combinedParentRolePermInfo) accumulatedRolePermission _permDel roleName source table PTDelete
|
2021-10-05 15:28:38 +03:00
|
|
|
let rolePermInfo = RolePermInfo roleInsertPermission roleSelectPermission roleUpdatePermission roleDeletePermission
|
|
|
|
returnA -< M.insert roleName rolePermInfo accumulatedRolePermMap
|
|
|
|
)
|
|
|
|
|) metadataRolePermissions (_unOrderedRoles orderedRoles)
|
2019-12-09 01:17:39 +03:00
|
|
|
where
|
2022-04-06 15:47:35 +03:00
|
|
|
mkMap :: [PermDef b e] -> HashMap RoleName (PermDef b e)
|
2020-12-08 17:22:31 +03:00
|
|
|
mkMap = mapFromL _pdRole
|
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
alignPermissions TablePermissionInputs {..} =
|
2020-12-08 17:22:31 +03:00
|
|
|
let insertsMap = M.map (\a -> ([a], [], [], [])) (mkMap _tpiInsert)
|
|
|
|
selectsMap = M.map (\a -> ([], [a], [], [])) (mkMap _tpiSelect)
|
|
|
|
updatesMap = M.map (\a -> ([], [], [a], [])) (mkMap _tpiUpdate)
|
|
|
|
deletesMap = M.map (\a -> ([], [], [], [a])) (mkMap _tpiDelete)
|
2021-09-24 01:56:37 +03:00
|
|
|
unionMap = M.unionWith (<>)
|
|
|
|
in insertsMap `unionMap` selectsMap `unionMap` updatesMap `unionMap` deletesMap
|
2019-12-09 01:17:39 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
mkPermissionMetadataObject ::
|
|
|
|
forall b a.
|
2022-04-06 15:47:35 +03:00
|
|
|
(BackendMetadata b) =>
|
2021-09-24 01:56:37 +03:00
|
|
|
SourceName ->
|
|
|
|
TableName b ->
|
2022-04-06 15:47:35 +03:00
|
|
|
PermDef b a ->
|
2021-09-24 01:56:37 +03:00
|
|
|
MetadataObject
|
2020-12-28 15:56:00 +03:00
|
|
|
mkPermissionMetadataObject source table permDef =
|
2022-04-06 15:47:35 +03:00
|
|
|
let permType = reflectPermDefPermission (_pdPermission permDef)
|
2021-09-24 01:56:37 +03:00
|
|
|
objectId =
|
|
|
|
MOSourceObjId source $
|
|
|
|
AB.mkAnyBackend $
|
|
|
|
SMOTableObj @b table $
|
|
|
|
MTOPerm (_pdRole permDef) permType
|
2021-04-22 00:44:37 +03:00
|
|
|
definition = toJSON $ WithTable @b source table permDef
|
2021-09-24 01:56:37 +03:00
|
|
|
in MetadataObject objectId definition
|
2019-12-09 01:17:39 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
mkRemoteSchemaPermissionMetadataObject ::
|
|
|
|
AddRemoteSchemaPermission ->
|
|
|
|
MetadataObject
|
2021-08-09 13:20:04 +03:00
|
|
|
mkRemoteSchemaPermissionMetadataObject (AddRemoteSchemaPermission rsName roleName defn _) =
|
2020-12-21 12:11:37 +03:00
|
|
|
let objectId = MORemoteSchemaPermissions rsName roleName
|
2021-09-24 01:56:37 +03:00
|
|
|
in MetadataObject objectId $ toJSON defn
|
2020-12-21 12:11:37 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
withPermission ::
|
|
|
|
forall bknd a b c s arr.
|
2022-04-06 15:47:35 +03:00
|
|
|
(ArrowChoice arr, ArrowWriter (Seq CollectedInfo) arr, BackendMetadata bknd) =>
|
2021-09-24 01:56:37 +03:00
|
|
|
WriterA (Seq SchemaDependency) (ErrorA QErr arr) (a, s) b ->
|
|
|
|
( a,
|
2022-04-06 15:47:35 +03:00
|
|
|
((SourceName, TableName bknd, PermDef bknd c, Proxy bknd), s)
|
2021-09-24 01:56:37 +03:00
|
|
|
)
|
|
|
|
`arr` (Maybe b)
|
2022-04-06 15:47:35 +03:00
|
|
|
withPermission f = proc (e, ((source, table, permDef, _proxy), s)) -> do
|
|
|
|
let metadataObject = mkPermissionMetadataObject @bknd source table permDef
|
|
|
|
permType = reflectPermDefPermission (_pdPermission permDef)
|
|
|
|
roleName = _pdRole permDef
|
2021-09-24 01:56:37 +03:00
|
|
|
schemaObject =
|
|
|
|
SOSourceObj source $
|
|
|
|
AB.mkAnyBackend $
|
|
|
|
SOITableObj @bknd table $
|
|
|
|
TOPerm roleName permType
|
2019-12-09 01:17:39 +03:00
|
|
|
addPermContext err = "in permission for role " <> roleName <<> ": " <> err
|
2021-09-24 01:56:37 +03:00
|
|
|
(|
|
|
|
|
withRecordInconsistency
|
|
|
|
( (|
|
|
|
|
withRecordDependencies
|
|
|
|
( (|
|
|
|
|
modifyErrA
|
|
|
|
(f -< (e, s))
|
|
|
|
|) (addTableContext @bknd table . addPermContext)
|
|
|
|
)
|
|
|
|
|) metadataObject schemaObject
|
|
|
|
)
|
|
|
|
|) metadataObject
|
2019-12-09 01:17:39 +03:00
|
|
|
|
2021-09-24 01:56:37 +03:00
|
|
|
buildPermission ::
|
|
|
|
forall b a arr m.
|
|
|
|
( ArrowChoice arr,
|
|
|
|
ArrowWriter (Seq CollectedInfo) arr,
|
|
|
|
Inc.ArrowCache m arr,
|
|
|
|
Inc.Cacheable (a b),
|
|
|
|
Inc.Cacheable (Proxy b),
|
|
|
|
MonadError QErr m,
|
2022-04-06 15:47:35 +03:00
|
|
|
BackendMetadata b
|
2021-09-24 01:56:37 +03:00
|
|
|
) =>
|
|
|
|
( Proxy b,
|
|
|
|
Inc.Dependency (TableCoreCache b),
|
|
|
|
SourceName,
|
|
|
|
TableName b,
|
|
|
|
FieldInfoMap (FieldInfo b),
|
2022-04-06 15:47:35 +03:00
|
|
|
Maybe (PermDef b a)
|
2021-09-24 01:56:37 +03:00
|
|
|
)
|
|
|
|
`arr` Maybe (PermInfo a b)
|
|
|
|
buildPermission = Inc.cache proc (proxy, tableCache, source, table, tableFields, maybePermission) ->
|
|
|
|
do
|
|
|
|
(|
|
|
|
|
traverseA
|
|
|
|
( \permission ->
|
|
|
|
(|
|
|
|
|
withPermission
|
|
|
|
( do
|
|
|
|
bindErrorA
|
|
|
|
-<
|
|
|
|
when (_pdRole permission == adminRoleName) $
|
|
|
|
throw400 ConstraintViolation "cannot define permission for admin role"
|
|
|
|
(info, dependencies) <-
|
|
|
|
liftEitherA <<< Inc.bindDepend
|
|
|
|
-<
|
|
|
|
runExceptT $
|
2022-04-06 15:47:35 +03:00
|
|
|
runTableCoreCacheRT (buildPermInfo source table tableFields (_pdPermission permission)) (source, tableCache)
|
2021-09-24 01:56:37 +03:00
|
|
|
tellA -< Seq.fromList dependencies
|
|
|
|
returnA -< info
|
|
|
|
)
|
|
|
|
|) (source, table, permission, proxy)
|
|
|
|
)
|
|
|
|
|) maybePermission
|
|
|
|
>-> (\info -> join info >- returnA)
|