server: new function permissions layer

Co-authored-by: Rikin Kachhia <54616969+rikinsk@users.noreply.github.com>
Co-authored-by: Rakesh Emmadi <12475069+rakeshkky@users.noreply.github.com>
GitOrigin-RevId: 35645121242294cb6bb500ea598e9a1f2ca67fa1
This commit is contained in:
Karthikeyan Chinnakonda 2021-01-29 11:18:17 +05:30 committed by hasura-bot
parent 0767333597
commit 10a3f9960d
57 changed files with 1271 additions and 298 deletions

View File

@ -6,7 +6,7 @@ echo "Running tests on node $CIRCLE_NODE_INDEX of $CIRCLE_NODE_TOTAL"
if [ -z "$SERVER_TEST_TO_RUN" ]; then
echo 'Please specify $SERVER_TEST_TO_RUN'
exit 1
else
else
echo "Running test $SERVER_TEST_TO_RUN"
fi
@ -522,7 +522,7 @@ case "$SERVER_TEST_TO_RUN" in
unset HASURA_GRAPHQL_CORS_DOMAIN
;;
ws-init-cookie-read-cors-enabled)
# test websocket transport with initial cookie header
@ -660,6 +660,22 @@ case "$SERVER_TEST_TO_RUN" in
kill_hge_servers
;;
function-permissions)
echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH FUNCTION PERMISSIONS ENABLED ########>\n"
TEST_TYPE="remote-schema-permissions"
export HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS=false
run_hge_with_args serve
wait_for_port 8080
pytest -n 1 -vv --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --test_function_permissions test_graphql_queries.py::TestGraphQLQueryFunctionPermissions
pytest -n 1 -vv --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --test_function_permissions test_graphql_mutations.py::TestGraphQLMutationFunctions
unset HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS
kill_hge_servers
;;
query-caching)
echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE QUERY CACHING #####################################>\n"
TEST_TYPE="query-caching"
@ -705,18 +721,18 @@ case "$SERVER_TEST_TO_RUN" in
if [ "$RUN_WEBHOOK_TESTS" == "true" ] ; then
TEST_TYPE="post-webhook"
echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET & WEBHOOK (POST) #########################>\n"
export HASURA_GRAPHQL_AUTH_HOOK="https://localhost:9090/"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
init_ssl
start_multiple_hge_servers
python3 webhook.py 9090 "$OUTPUT_FOLDER/ssl/webhook-key.pem" "$OUTPUT_FOLDER/ssl/webhook.pem" > "$OUTPUT_FOLDER/webhook.log" 2>&1 & WH_PID=$!
wait_for_port 9090
run_pytest_parallel --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --hge-webhook="$HASURA_GRAPHQL_AUTH_HOOK"
kill_hge_servers
fi
;;

View File

@ -60,6 +60,14 @@ access over it.
either with the server flag ``--enable-remote-schema-permissions`` or the environment
variable ``HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS`` set to ``true``.
### Function Permissions
Before volatile functions were supported, the permissions for functions were automatically inferred
from the select permission of the target table. Now, since volatile functions are supported we can't
do this anymore, so function permissions are introduced which will explicitly grant permission to
a function for a given role. A pre-requisite to adding a function permission is that the role should
have select permissions to the target table of the function.
### Breaking changes
- This release contains the [PDV refactor (#4111)](https://github.com/hasura/graphql-engine/pull/4111), a significant rewrite of the internals of the server, which did include some breaking changes:
@ -69,12 +77,12 @@ variable ``HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS`` set to ``true``.
- if a query selects table `bar` through table `foo` via a relationship, the required permissions headers will be the union of the required headers of table `foo` and table `bar` (we used to only check the headers of the root table);
- if an insert does not have an `on_conflict` clause, it will not require the update permissions headers.
This release contains the remote schema permissions feature, which introduces a breaking change:
- This release contains the remote schema permissions feature, which introduces a breaking change:
Earlier, remote schemas were considered to be a public entity and all the roles had unrestricted
access to the remote schema. If remote schema permissions are enabled in the graphql-engine, a given
remote schema will only be accessible to a role ,if the role has permissions configured for the said remote schema
and be accessible according to the permissions that were configured for the role.
Earlier, remote schemas were considered to be a public entity and all the roles had unrestricted
access to the remote schema. If remote schema permissions are enabled in the graphql-engine, a given
remote schema will only be accessible to a role ,if the role has permissions configured for the said remote schema
and be accessible according to the permissions that were configured for the role.
### Bug fixes and improvements

View File

@ -15,29 +15,33 @@ API Reference
Available APIs
--------------
+-----------------+-----------------------------------------+------------------+
| API | Endpoint | Access |
+=================+=========================================+==================+
| GraphQL | :ref:`/v1/graphql <graphql_api>` | Permission rules |
+-----------------+-----------------------------------------+------------------+
| Relay | :ref:`/v1beta1/relay <relay_api>` | Permission rules |
+-----------------+-----------------------------------------+------------------+
| Legacy GraphQL | :ref:`/v1alpha1/graphql <graphql_api>` | Permission rules |
+-----------------+-----------------------------------------+------------------+
| Schema/Metadata | :ref:`/v1/query <schema_metadata_api>` | Admin only |
+-----------------+-----------------------------------------+------------------+
| RESTified GQL | :ref:`/api/rest <restified_api>` | GQL REST Routes |
+-----------------+-----------------------------------------+------------------+
| Version | :ref:`/v1/version <version_api>` | Public |
+-----------------+-----------------------------------------+------------------+
| Health | :ref:`/healthz <health_api>` | Public |
+-----------------+-----------------------------------------+------------------+
| PG Dump | :ref:`/v1alpha1/pg_dump <pg_dump_api>` | Admin only |
+-----------------+-----------------------------------------+------------------+
| Config | :ref:`/v1alpha1/config <config_api>` | Admin only |
+-----------------+-----------------------------------------+------------------+
| Explain | :ref:`/v1/graphql/explain <explain_api>`| Admin only |
+-----------------+-----------------------------------------+------------------+
+----------------------------------+---------------------------------------------------+------------------+
| API | Endpoint | Access |
+==================================+===================================================+==================+
| GraphQL | :ref:`/v1/graphql <graphql_api>` | Permission rules |
+----------------------------------+---------------------------------------------------+------------------+
| Relay | :ref:`/v1beta1/relay <relay_api>` | Permission rules |
+----------------------------------+---------------------------------------------------+------------------+
| Legacy GraphQL | :ref:`/v1alpha1/graphql <graphql_api>` | Permission rules |
+----------------------------------+---------------------------------------------------+------------------+
| Schema/Metadata *(< v1.3)* | :ref:`/v1/query <schema_metadata_api>` | Admin only |
+----------------------------------+---------------------------------------------------+------------------+
| Schema *(> v1.4)* | :ref:`/v2/query <schema_api>` | Admin only |
+----------------------------------+---------------------------------------------------+------------------+
| Metadata *(> v1.4)* | :ref:`/v1/metadata <metadata_api>` | Admin only |
+----------------------------------+---------------------------------------------------+------------------+
| Restified GQL | :ref:`/api/rest <restified_api>` | GQL REST Routes |
+----------------------------------+---------------------------------------------------+------------------+
| Version | :ref:`/v1/version <version_api>` | Public |
+----------------------------------+---------------------------------------------------+------------------+
| Health | :ref:`/healthz <health_api>` | Public |
+----------------------------------+---------------------------------------------------+------------------+
| PG Dump | :ref:`/v1alpha1/pg_dump <pg_dump_api>` | Admin only |
+----------------------------------+---------------------------------------------------+------------------+
| Config | :ref:`/v1alpha1/config <config_api>` | Admin only |
+----------------------------------+---------------------------------------------------+------------------+
| Explain | :ref:`/v1/graphql/explain <explain_api>` | Admin only |
+----------------------------------+---------------------------------------------------+------------------+
.. _graphql_api:
@ -61,14 +65,36 @@ See details at :ref:`api_reference_relay_graphql`.
.. _schema_metadata_api:
Schema / metadata API
^^^^^^^^^^^^^^^^^^^^^
Schema / metadata API V1 (v1.3 and below)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Hasura exposes a schema / metadata API for managing metadata for permissions/relationships or for directly
executing SQL on the underlying Postgres.
This is primarily intended to be used as an ``admin`` API to manage the Hasura schema and metadata.
See details at :ref:`schema_metadata_apis` .
.. _schema_api:
Schema API (v1.4 and above)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Hasura exposes a schema API for directly executing SQL on the underlying Postgres.
This is primarily intended to be used as an ``admin`` API to manage the Hasura schema.
See details at :ref:`schema_apis`.
.. _metadata_api:
Metadata API (v1.4 and above)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Hasura exposes a metadata API for managing metadata.
This is primarily intended to be used as an ``admin`` API to manage the Hasura metadata.
See details at :ref:`metadata_apis`.
.. _version_api:
@ -142,7 +168,9 @@ You can refer to the following to know about all PostgreSQL types supported by t
GraphQL API <graphql-api/index>
Relay GraphQL API <relay-graphql-api/index>
Schema / Metadata APIs <schema-metadata-api/index>
Schema / Metadata APIs V1 <schema-metadata-api/index>
Schema APIs <schema-api/index>
Metadata APIs <metadata-api/index>
RESTified GraphQL Endpoints <restified>
Version API <version>
Health check API <health>

View File

@ -0,0 +1,62 @@
Status Code,Code,Error
400,postgres-error,Not-NULL violation. null value in column <column-name> violates not-null constraint
400,permission-denied,select on <column/table> for role <role-name> is not allowed.
400,not-exists,table <table-name> does not exist
400,not-exists,no such table/view exists in postgres : <table-name>
400,not-exists,<field-name> does not exist
400,already-tracked,view/table already tracked : <table-name>
400,access-denied,restricted access : admin only
400,not-supported,table renames are not yet supported : <table-name>
400,not-exists,<column-name> does not exist
400,already-exists,cannot add column <column-name> in table <table-name> as a relationship with the name already exists
400,invalid-json,invalid json
400,not-supported,column renames are not yet supported : <table-name>.<column-name>
400,invalid-headers,missing header : <header-name>
400,dependency-error,cannot change type of column <column-name> in table <table-name> because of the following dependencies : <dependencies>
400,invalid-headers,X-Hasura-User-Id should be an integer
400,dependency-error,cannot drop due to the following dependent objects : <dependencies>
400,access-denied,You have to be admin to access this endpoint
400,parse-failed,parsing dotted table failed : <table-name>
400,access-denied,not authorised to access this tx
400,already-exists,multiple declarations exist for the following <table-name> : <duplicates>
400,not-exists,tx does not exists
400,already-exists,column/relationship of table <table-name> already exists
400,already-initialised,the state seems to be initialised already. \ \ you may need to migrate from this version: <catalog-version>
400,constraint-error,no foreign constraint exists on the given column
400,not-supported,unsupported version : <catalog-version>
400,constraint-error,more than one foreign key constraint exists on the given column
400,already-exists,the query template already exists <template-name>
400,permission-error,<permission-type>' permission on <table-name> for role <role-name> already exists
400,permission-error,<permission-type>' permission on <table-name> for role <role-name> does not exist
400,unexpected-payload,Unknown operator : <operator-type>
400,unexpected-payload,expecting a string for column operator
400,unexpected-payload,"incompatible column types : '<column-name>', '<column-name>' "
400,unexpected-payload,Expecting 'constraint' or 'constraint_on' when the 'action' is 'update'
400,unexpected-payload,constraint' and 'constraint_on' cannot be set at a time
400,unexpected-payload,upsert is not allowed for role '<role-name>'
400,unexpected-payload,objects should not be empty
400,invalid-params,missing parameter : <param-name>
400,unexpected-payload,can't be empty
400,,<col-name>' is a relationship and should be expanded
400,unexpected-payload,<column-name>' should be included in 'columns'
400,unexpected-payload,<column-name>' is an array relationship and can't be used in 'order_by'
400,,<column-name>' is a Postgres column and cannot be chained further
400,unexpected-payload,order_by array should not be empty
400,unexpected-payload,"when selecting an 'obj_relationship' 'where', 'order_by', 'limit' and 'offset' can't be used"
400,unexpected-payload,"atleast one of $set, $inc, $mul has to be present"
400,permission-denied,<permission-type> on <table-name> for role <role-name> is not allowed
400,not-exists,no such column exists : <column-name>
400,permission-denied,role <role-name> does not have permission to <permission-type> column <column-name>
400,,"expecting a postgres column; but, <name> is relationship"
400,unexpected-payload,JSON column can not be part of where clause
400,unexpected-payload,is of type <type-name>; this operator works only on column of types <[types]>
400,postgres-error,query execution failed
500,unexpected,unexpected dependency of relationship : <dependency>
500,unexpected,unexpected dependent object : <dependency>
500,unexpected,field already exists
500,unexpected,field does not exist
500,unexpected,permission does not exist
500,postgres-error,postgres transaction error
500,postgres-error,connection error
500,postgres-error,postgres query error
404,not-found,No such resource exists
1 Status Code Code Error
2 400 postgres-error Not-NULL violation. null value in column <column-name> violates not-null constraint
3 400 permission-denied select on <column/table> for role <role-name> is not allowed.
4 400 not-exists table <table-name> does not exist
5 400 not-exists no such table/view exists in postgres : <table-name>
6 400 not-exists <field-name> does not exist
7 400 already-tracked view/table already tracked : <table-name>
8 400 access-denied restricted access : admin only
9 400 not-supported table renames are not yet supported : <table-name>
10 400 not-exists <column-name> does not exist
11 400 already-exists cannot add column <column-name> in table <table-name> as a relationship with the name already exists
12 400 invalid-json invalid json
13 400 not-supported column renames are not yet supported : <table-name>.<column-name>
14 400 invalid-headers missing header : <header-name>
15 400 dependency-error cannot change type of column <column-name> in table <table-name> because of the following dependencies : <dependencies>
16 400 invalid-headers X-Hasura-User-Id should be an integer
17 400 dependency-error cannot drop due to the following dependent objects : <dependencies>
18 400 access-denied You have to be admin to access this endpoint
19 400 parse-failed parsing dotted table failed : <table-name>
20 400 access-denied not authorised to access this tx
21 400 already-exists multiple declarations exist for the following <table-name> : <duplicates>
22 400 not-exists tx does not exists
23 400 already-exists column/relationship of table <table-name> already exists
24 400 already-initialised the state seems to be initialised already. \ \ you may need to migrate from this version: <catalog-version>
25 400 constraint-error no foreign constraint exists on the given column
26 400 not-supported unsupported version : <catalog-version>
27 400 constraint-error more than one foreign key constraint exists on the given column
28 400 already-exists the query template already exists <template-name>
29 400 permission-error <permission-type>' permission on <table-name> for role <role-name> already exists
30 400 permission-error <permission-type>' permission on <table-name> for role <role-name> does not exist
31 400 unexpected-payload Unknown operator : <operator-type>
32 400 unexpected-payload expecting a string for column operator
33 400 unexpected-payload incompatible column types : '<column-name>', '<column-name>'
34 400 unexpected-payload Expecting 'constraint' or 'constraint_on' when the 'action' is 'update'
35 400 unexpected-payload constraint' and 'constraint_on' cannot be set at a time
36 400 unexpected-payload upsert is not allowed for role '<role-name>'
37 400 unexpected-payload objects should not be empty
38 400 invalid-params missing parameter : <param-name>
39 400 unexpected-payload can't be empty
40 400 <col-name>' is a relationship and should be expanded
41 400 unexpected-payload <column-name>' should be included in 'columns'
42 400 unexpected-payload <column-name>' is an array relationship and can't be used in 'order_by'
43 400 <column-name>' is a Postgres column and cannot be chained further
44 400 unexpected-payload order_by array should not be empty
45 400 unexpected-payload when selecting an 'obj_relationship' 'where', 'order_by', 'limit' and 'offset' can't be used
46 400 unexpected-payload atleast one of $set, $inc, $mul has to be present
47 400 permission-denied <permission-type> on <table-name> for role <role-name> is not allowed
48 400 not-exists no such column exists : <column-name>
49 400 permission-denied role <role-name> does not have permission to <permission-type> column <column-name>
50 400 expecting a postgres column; but, <name> is relationship
51 400 unexpected-payload JSON column can not be part of where clause
52 400 unexpected-payload is of type <type-name>; this operator works only on column of types <[types]>
53 400 postgres-error query execution failed
54 500 unexpected unexpected dependency of relationship : <dependency>
55 500 unexpected unexpected dependent object : <dependency>
56 500 unexpected field already exists
57 500 unexpected field does not exist
58 500 unexpected permission does not exist
59 500 postgres-error postgres transaction error
60 500 postgres-error connection error
61 500 postgres-error postgres query error
62 404 not-found No such resource exists

View File

@ -0,0 +1,164 @@
.. meta::
:description: Hasura metadata API reference
:keywords: hasura, docs, metadata API, API reference
.. _metadata_apis:
Metadata API Reference (v1.4 and above)
=======================================
.. contents:: Table of contents
:backlinks: none
:depth: 1
:local:
Introduction
------------
This is primarily intended to be used as an ``admin`` API to manage the Hasura metadata.
Endpoint
--------
All requests are ``POST`` requests to the ``/v1/metadata`` endpoint.
Request structure
-----------------
.. code-block:: http
POST /v1/metadata HTTP/1.1
{
"type": "<query-type>",
"args": <args-object>
}
Request body
^^^^^^^^^^^^
.. parsed-literal::
Query_
.. _Query:
Query
*****
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - type
- true
- String
- Type of the query
* - args
- true
- JSON Value
- The arguments to the query
* - version
- false
- Integer
- Version of the API (default: 1)
Request types
-------------
The various types of queries are listed in the following table:
.. list-table::
:header-rows: 1
* - ``type``
- ``args``
- ``version``
- Synopsis
* - **bulk**
- :ref:`Query <Query>` array
- 1
- Execute multiple operations in a single query
* - :ref:`pg_create_function_permission`
- :ref:`pg_create_function_permission_args <pg_create_function_permission_args_syntax>`
- 1
- Create a function permission
* - :ref:`pg_drop_function_permission`
- :ref:`pg_drop_function_permission_args <pg_drop_function_permission_args_syntax>`
- 1
- Drop an existing function permission
Response structure
------------------
.. list-table::
:widths: 10 10 30
:header-rows: 1
* - Status code
- Description
- Response structure
* - ``200``
- Success
- .. parsed-literal::
Request specific
* - ``400``
- Bad request
- .. code-block:: haskell
{
"path" : String,
"error" : String
}
* - ``401``
- Unauthorized
- .. code-block:: haskell
{
"error" : String
}
* - ``500``
- Internal server error
- .. code-block:: haskell
{
"error" : String
}
Error codes
-----------
.. csv-table::
:file: dataerrors.csv
:widths: 10, 20, 70
:header-rows: 1
Disabling metadata API
----------------------
Since this API can be used to make changes to the GraphQL schema, it can be
disabled, especially in production deployments.
The ``enabled-apis`` flag or the ``HASURA_GRAPHQL_ENABLED_APIS`` env var can be used to
enable/disable this API. By default, the schema/metadata API is enabled. To disable it, you need
to explicitly state that this API is not enabled i.e. remove it from the list of enabled APIs.
.. code-block:: bash
# enable only graphql api, disable metadata and pgdump
--enabled-apis="graphql"
HASURA_GRAPHQL_ENABLED_APIS="graphql"
See :ref:`server_flag_reference` for info on setting the above flag/env var.

View File

@ -0,0 +1,154 @@
.. meta::
:description: Hasura schema API reference
:keywords: hasura, docs, schema API, API reference
.. _schema_apis:
Schema API Reference (v1.4 and above)
=====================================
.. contents:: Table of contents
:backlinks: none
:depth: 1
:local:
Introduction
------------
The schema API provides the following features:
1. Execute SQL on the underlying Postgres database, supports schema modifying actions.
This is primarily intended to be used as an ``admin`` API to manage the Hasura schema.
Endpoint
--------
All requests are ``POST`` requests to the ``/v2/query`` endpoint.
Request structure
-----------------
.. code-block:: http
POST /v1/query HTTP/1.1
{
"type": "<query-type>",
"args": <args-object>
}
Request body
^^^^^^^^^^^^
.. parsed-literal::
Query_
.. _Query_:
Query
*****
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - type
- true
- String
- Type of the query
* - args
- true
- JSON Value
- The arguments to the query
* - version
- false
- Integer
- Version of the API (default: 1)
Request types
-------------
The various types of queries are listed in the following table:
.. list-table::
:header-rows: 1
* - ``type``
- ``args``
- ``version``
- Synopsis
* - **bulk**
- :ref:`Query <Query>` array
- 1
- Execute multiple operations in a single query
* - :ref:`run_sql`
- :ref:`run_sql_args <run_sql_syntax>`
- 1
- Run SQL directly on Postgres
Response structure
------------------
.. list-table::
:widths: 10 10 30
:header-rows: 1
* - Status code
- Description
- Response structure
* - ``200``
- Success
- .. parsed-literal::
Request specific
* - ``400``
- Bad request
- .. code-block:: haskell
{
"path" : String,
"error" : String
}
* - ``401``
- Unauthorized
- .. code-block:: haskell
{
"error" : String
}
* - ``500``
- Internal server error
- .. code-block:: haskell
{
"error" : String
}
Disabling schema API
--------------------
Since this API can be used to make changes to the GraphQL schema, it can be
disabled, especially in production deployments.
The ``enabled-apis`` flag or the ``HASURA_GRAPHQL_ENABLED_APIS`` env var can be used to
enable/disable this API. By default, the schema/metadata API is enabled. To disable it, you need
to explicitly state that this API is not enabled i.e. remove it from the list of enabled APIs.
.. code-block:: bash
# enable only graphql api, disable metadata and pgdump
--enabled-apis="graphql"
HASURA_GRAPHQL_ENABLED_APIS="graphql"
See :ref:`server_flag_reference` for info on setting the above flag/env var.

View File

@ -107,7 +107,7 @@ In most cases you will want ``VOLATILE`` functions to only be exposed as
mutations, and only ``STABLE`` and ``IMMUTABLE`` functions to be queries.
When tracking ``VOLATILE`` functions under the ``query`` root, the user needs
to guarantee that the field is idempotent and side-effect free, in the context
of the resulting GraphQL API.
of the resulting GraphQL API.
One such use case might be a function that wraps a simple query and performs
some logging visible only to administrators.
@ -170,6 +170,102 @@ Function Configuration
- **Return type**: MUST be ``SETOF <table-name>`` where ``<table-name>`` is already tracked
- **Argument modes**: ONLY ``IN``
.. _pg_create_function_permission:
pg_create_function_permission
-----------------------------
``pg_create_function_permission`` is used to add permission to an existing custom function.
To add a function permission, the graphql-engine should have disabled inferring of
function permissions and the provided role should have select permissions to the
target table of the function.
.. code-block:: http
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "pg_create_function_permission",
"args": {
"function": "get_articles",
"role": "user"
}
}
.. _pg_create_function_permission_args_syntax:
Args syntax
^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - function
- true
- :ref:`FunctionName <FunctionName>`
- Name of the SQL function
* - role
- true
- :ref:`RoleName <RoleName>`
- Name of the role
* - source
- false
- Text
- Name of the source of the SQL function
.. _pg_drop_function_permission:
pg_drop_function_permission
---------------------------
``pg_drop_function_permission`` is used to drop an existing function permission.
.. code-block:: http
POST /v1/metadata HTTP/1.1
Content-Type: application/json
X-Hasura-Role: admin
{
"type": "pg_drop_function_permission",
"args": {
"function": "get_articles",
"role": "user"
}
}
.. _pg_drop_function_permission_args_syntax:
Args syntax
^^^^^^^^^^^
.. list-table::
:header-rows: 1
* - Key
- Required
- Schema
- Description
* - function
- true
- :ref:`FunctionName <FunctionName>`
- Name of the SQL function
* - role
- true
- :ref:`RoleName <RoleName>`
- Name of the role
* - source
- false
- Text
- Name of the source of the SQL function
.. _untrack_function:
untrack_function

View File

@ -2,10 +2,10 @@
:description: Hasura schema/metadata API reference
:keywords: hasura, docs, schema/metadata API, API reference
.. _metadata_apis:
.. _schema_metadata_apis:
Schema / Metadata API Reference
===============================
Schema / Metadata API Reference (v1.3 and below)
================================================
.. contents:: Table of contents
:backlinks: none

View File

@ -177,7 +177,7 @@ API should be called with the schema document.
}
Argument Presets
^^^^^^^^^^^^^^^^^
^^^^^^^^^^^^^^^^
Argument presets can be used to automatically inject input values for fields
during execution. This way the field is executed with limited input values. Argument
@ -344,7 +344,7 @@ Args syntax
.. _RemoteSchemaPermission:
RemoteSchemaPermission
&&&&&&&&&&&&&&&&&&&&&&
""""""""""""""""""""""
.. list-table::
:header-rows: 1

View File

@ -227,6 +227,16 @@ For the ``serve`` sub-command these are the available flags and ENV variables:
- ``HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS``
- Enable remote schema permissions (default: ``false``)
* - ``--infer-function-permissions``
- ``HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS``
- When the ``--infer-function-permissions`` flag is set to ``false``, a function ``f``, stable, immutable or volatile is
only exposed for a role ``r`` if there is a permission defined on the function ``f`` for the role ``r``, creating a
function permission will only be allowed if there is a select permission on the table type.
When the ``--infer-function-permissions`` flag is set to ``true`` or the flag is omitted (defaults to ``true``), the
permission of the function is inferred from the select permissions from the target table of the function, only for
stable/immutable functions. Volatile functions are not exposed to any of the roles in this case.
.. note::
When the equivalent flags for environment variables are used, the flags will take precedence.

View File

@ -583,3 +583,8 @@ Permissions for custom function queries
**For example**, in our text-search example above, if the role ``user`` doesn't have the requisite permissions to view
the table ``article``, a validation error will be thrown if the ``search_articles`` query is run using the ``user``
role.
.. note::
When inferring of function permissions is disabled, then there should be a function permission configured for the function to
be accessible to a role, otherwise the function is not exposed to the role.

View File

@ -101,12 +101,14 @@ runApp env (HGEOptionsG rci metadataDbUrl hgeCmd) = do
remoteSchemaPermsCtx = RemoteSchemaPermsDisabled
pgLogger = print
pgSourceResolver = mkPgSourceResolver pgLogger
cacheBuildParams = CacheBuildParams _gcHttpManager sqlGenCtx remoteSchemaPermsCtx pgSourceResolver
functionPermsCtx = FunctionPermissionsInferred
serverConfigCtx = ServerConfigCtx functionPermsCtx remoteSchemaPermsCtx sqlGenCtx
cacheBuildParams = CacheBuildParams _gcHttpManager pgSourceResolver serverConfigCtx
runManagedT (mkMinimalPool _gcMetadataDbConnInfo) $ \metadataDbPool -> do
res <- flip runPGMetadataStorageApp (metadataDbPool, pgLogger) $
runMetadataStorageT $ liftEitherM do
metadata <- fetchMetadata
runAsAdmin sqlGenCtx _gcHttpManager remoteSchemaPermsCtx $ do
runAsAdmin sqlGenCtx _gcHttpManager remoteSchemaPermsCtx functionPermsCtx $ do
schemaCache <- runCacheBuild cacheBuildParams $
buildRebuildableSchemaCache env metadata
execQuery env queryBs

View File

@ -275,7 +275,7 @@ initialiseServeCtx env GlobalCtx{..} so@ServeOptions{..} = do
(rebuildableSchemaCache, cacheInitStartTime) <-
lift . flip onException (flushLogger loggerCtx) $
migrateCatalogSchema env logger metadataDbPool maybeDefaultSourceConfig _gcHttpManager
sqlGenCtx soEnableRemoteSchemaPermissions (mkPgSourceResolver pgLogger)
sqlGenCtx soEnableRemoteSchemaPermissions soInferFunctionPermissions (mkPgSourceResolver pgLogger)
let schemaSyncCtx = SchemaSyncCtx schemaSyncListenerThread schemaSyncEventRef cacheInitStartTime
pure $ ServeCtx _gcHttpManager instanceId loggers metadataDbPool latch
@ -297,14 +297,19 @@ mkLoggers enabledLogs logLevel = do
migrateCatalogSchema
:: (HasVersion, MonadIO m, MonadBaseControl IO m)
=> Env.Environment -> Logger Hasura -> Q.PGPool -> Maybe SourceConfiguration
-> HTTP.Manager -> SQLGenCtx -> RemoteSchemaPermsCtx -> SourceResolver
-> HTTP.Manager -> SQLGenCtx -> RemoteSchemaPermsCtx -> FunctionPermissionsCtx
-> SourceResolver
-> m (RebuildableSchemaCache, UTCTime)
migrateCatalogSchema env logger pool defaultSourceConfig httpManager sqlGenCtx remoteSchemaPermsCtx sourceResolver = do
migrateCatalogSchema env logger pool defaultSourceConfig
httpManager sqlGenCtx remoteSchemaPermsCtx functionPermsCtx
sourceResolver = do
currentTime <- liftIO Clock.getCurrentTime
initialiseResult <- runExceptT $ do
(migrationResult, metadata) <- Q.runTx pool (Q.Serializable, Just Q.ReadWrite) $
migrateCatalog defaultSourceConfig currentTime
let cacheBuildParams = CacheBuildParams httpManager sqlGenCtx remoteSchemaPermsCtx sourceResolver
let serverConfigCtx = ServerConfigCtx functionPermsCtx remoteSchemaPermsCtx sqlGenCtx
cacheBuildParams =
CacheBuildParams httpManager sourceResolver serverConfigCtx
buildReason = case getMigratedFrom migrationResult of
Nothing -> CatalogSync
Just version ->
@ -451,6 +456,7 @@ runHGEServer env ServeOptions{..} ServeCtx{..} initTime postPollHook serverMetri
_scSchemaCache
ekgStore
soEnableRemoteSchemaPermissions
soInferFunctionPermissions
soConnectionOptions
soWebsocketKeepAlive
@ -462,6 +468,7 @@ runHGEServer env ServeOptions{..} ServeCtx{..} initTime postPollHook serverMetri
_ <- startSchemaSyncProcessorThread sqlGenCtx
logger _scHttpManager _sscSyncEventRef
cacheRef _scInstanceId _sscCacheInitStartTime soEnableRemoteSchemaPermissions
soInferFunctionPermissions
let
maxEvThrds = fromMaybe defaultMaxEventThreads soEventsHttpPoolSize
@ -613,10 +620,12 @@ runAsAdmin
:: SQLGenCtx
-> HTTP.Manager
-> RemoteSchemaPermsCtx
-> FunctionPermissionsCtx
-> RunT m a
-> m (Either QErr a)
runAsAdmin sqlGenCtx httpManager remoteSchemaPermsCtx m = do
let runCtx = RunCtx adminUserInfo httpManager sqlGenCtx remoteSchemaPermsCtx
runAsAdmin sqlGenCtx httpManager remoteSchemaPermsCtx functionPermsCtx m = do
let serverConfigCtx = ServerConfigCtx functionPermsCtx remoteSchemaPermsCtx sqlGenCtx
runCtx = RunCtx adminUserInfo httpManager serverConfigCtx
runExceptT $ peelRun runCtx m
execQuery
@ -626,10 +635,9 @@ execQuery
, MonadBaseControl IO m
, MonadUnique m
, HasHttpManagerM m
, HasSQLGenCtx m
, UserInfoM m
, Tracing.MonadTrace m
, HasRemoteSchemaPermsCtx m
, HasServerConfigCtx m
, MetadataM m
, MonadMetadataStorageQueryAPI m
)

View File

@ -77,8 +77,7 @@ buildGQLContext
, MonadError QErr m
, MonadIO m
, MonadUnique m
, HasSQLGenCtx m
, HasRemoteSchemaPermsCtx m
, HasServerConfigCtx m
)
=> ( GraphQLQueryType
, SourceCache
@ -92,9 +91,8 @@ buildGQLContext
)
buildGQLContext =
proc (queryType, pgSources, allRemoteSchemas, allActions, nonObjectCustomTypes) -> do
sqlGenCtx@(SQLGenCtx{ stringifyNum }) <- bindA -< askSQLGenCtx
remoteSchemaPermsCtx <- bindA -< askRemoteSchemaPermsCtx
ServerConfigCtx functionPermsCtx remoteSchemaPermsCtx sqlGenCtx@(SQLGenCtx stringifyNum) <-
bindA -< askServerConfigCtx
let remoteSchemasRoles = concatMap (Map.keys . _rscPermissions . fst . snd) $ Map.toList allRemoteSchemas
@ -108,7 +106,9 @@ buildGQLContext =
allRemoteSchemas
<&> (\(remoteSchemaCtx, _metadataObj) ->
(_rscIntro remoteSchemaCtx, _rscParsed remoteSchemaCtx))
adminQueryContext = QueryContext stringifyNum queryType adminRemoteRelationshipQueryCtx
-- The function permissions context doesn't actually matter because the
-- admin will have access to the function anyway
adminQueryContext = QueryContext stringifyNum queryType adminRemoteRelationshipQueryCtx FunctionPermissionsInferred
-- build the admin DB-only context so that we can check against name clashes with remotes
-- TODO: Is there a better way to check for conflicts without actually building the admin schema?
@ -142,10 +142,10 @@ buildGQLContext =
( Set.toMap allRoles & Map.traverseWithKey \roleName () ->
case queryType of
QueryHasura ->
buildRoleContext (sqlGenCtx, queryType) pgSources allRemoteSchemas allActionInfos
buildRoleContext (sqlGenCtx, queryType, functionPermsCtx) pgSources allRemoteSchemas allActionInfos
nonObjectCustomTypes remotes roleName remoteSchemaPermsCtx
QueryRelay ->
buildRelayRoleContext (sqlGenCtx, queryType) pgSources allActionInfos
buildRelayRoleContext (sqlGenCtx, queryType, functionPermsCtx) pgSources allActionInfos
nonObjectCustomTypes adminMutationRemotes roleName
)
unauthenticated <- bindA -< unauthenticatedContext adminQueryRemotes adminMutationRemotes remoteSchemaPermsCtx
@ -187,13 +187,13 @@ buildRoleBasedRemoteSchemaParser role remoteSchemaCache = do
-- TODO: Integrate relay schema
buildRoleContext
:: (MonadError QErr m, MonadIO m, MonadUnique m)
=> (SQLGenCtx, GraphQLQueryType) -> SourceCache -> RemoteSchemaCache
=> (SQLGenCtx, GraphQLQueryType, FunctionPermissionsCtx) -> SourceCache -> RemoteSchemaCache
-> [ActionInfo 'Postgres] -> NonObjectTypeMap
-> [( RemoteSchemaName , (IntrospectionResult, ParsedIntrospection))]
-> RoleName
-> RemoteSchemaPermsCtx
-> m (RoleContext GQLContext)
buildRoleContext (SQLGenCtx stringifyNum, queryType) sources
buildRoleContext (SQLGenCtx stringifyNum, queryType, functionPermsCtx) sources
allRemoteSchemas allActionInfos nonObjectCustomTypes remotes roleName remoteSchemaPermsCtx = do
roleBasedRemoteSchemas <-
@ -206,7 +206,7 @@ buildRoleContext (SQLGenCtx stringifyNum, queryType) sources
let queryRemotes = getQueryRemotes $ snd . snd <$> roleBasedRemoteSchemas
mutationRemotes = getMutationRemotes $ snd . snd <$> roleBasedRemoteSchemas
remoteRelationshipQueryContext = Map.fromList roleBasedRemoteSchemas
roleQueryContext = QueryContext stringifyNum queryType remoteRelationshipQueryContext
roleQueryContext = QueryContext stringifyNum queryType remoteRelationshipQueryContext functionPermsCtx
runMonadSchema roleName roleQueryContext sources $ do
let pgSources = mapMaybe unsafeSourceInfo $ toList sources
@ -252,7 +252,6 @@ buildRoleContext (SQLGenCtx stringifyNum, queryType) sources
-> [P.FieldParser (P.ParseT Identity) RemoteField]
getMutationRemotes = concatMap (concat . piMutation)
buildFullestDBSchema
:: (MonadError QErr m, MonadIO m, MonadUnique m)
=> QueryContext -> SourceCache -> [ActionInfo 'Postgres] -> NonObjectTypeMap
@ -287,16 +286,16 @@ buildFullestDBSchema queryContext sources allActionInfos nonObjectCustomTypes =
buildRelayRoleContext
:: (MonadError QErr m, MonadIO m, MonadUnique m)
=> (SQLGenCtx, GraphQLQueryType) -> SourceCache -> [ActionInfo 'Postgres] -> NonObjectTypeMap
=> (SQLGenCtx, GraphQLQueryType, FunctionPermissionsCtx) -> SourceCache -> [ActionInfo 'Postgres] -> NonObjectTypeMap
-> [P.FieldParser (P.ParseT Identity) RemoteField]
-> RoleName
-> m (RoleContext GQLContext)
buildRelayRoleContext (SQLGenCtx stringifyNum, queryType) sources
buildRelayRoleContext (SQLGenCtx stringifyNum, queryType, functionPermsCtx) sources
allActionInfos nonObjectCustomTypes mutationRemotes roleName =
-- TODO: At the time of writing this, remote schema queries are not supported in relay.
-- When they are supported, we should get do what `buildRoleContext` does. Since, they
-- are not supported yet, we use `mempty` below for `RemoteRelationshipQueryContext`.
let roleQueryContext = QueryContext stringifyNum queryType mempty
let roleQueryContext = QueryContext stringifyNum queryType mempty functionPermsCtx
in
runMonadSchema roleName roleQueryContext sources $ do
@ -432,6 +431,8 @@ buildQueryFields
-> [FunctionInfo b]
-> m [P.FieldParser n (QueryRootField (UnpreparedValue b))]
buildQueryFields sourceName sourceConfig tables (takeExposedAs FEAQuery id -> functions) = do
functionPermsCtx <- asks $ qcFunctionPermsContext . getter
roleName <- askRoleName
tableSelectExpParsers <- for tables \(table, _tableInfo) -> do
selectPerms <- tableSelectPermissions table
customRootFields <- _tcCustomRootFields . _tciCustomConfig . _tiCoreInfo <$> askTableInfo @'Postgres table
@ -447,32 +448,38 @@ buildQueryFields sourceName sourceConfig tables (takeExposedAs FEAQuery id -> fu
, mapMaybeFieldParser (asDbRootField . QDBPrimaryKey) $ selectTableByPk table (fromMaybe pkName $ _tcrfSelectByPk customRootFields) (Just pkDesc) perms
, mapMaybeFieldParser (asDbRootField . QDBAggregation) $ selectTableAggregate table (fromMaybe aggName $ _tcrfSelectAggregate customRootFields) (Just aggDesc) perms
]
functionSelectExpParsers <- for functions \function -> do
let targetTable = fiReturnType function
functionName = fiName function
selectPerms <- tableSelectPermissions targetTable
for selectPerms \perms -> do
displayName <- functionGraphQLName @b functionName `onLeft` throwError
let functionDesc = G.Description $ "execute function " <> functionName <<> " which returns " <>> targetTable
aggName = displayName <> $$(G.litName "_aggregate")
aggDesc = G.Description $ "execute function " <> functionName <<> " and query aggregates on result of table type " <>> targetTable
catMaybes <$> sequenceA
[ requiredFieldParser (asDbRootField . QDBSimple) $ selectFunction function displayName (Just functionDesc) perms
, mapMaybeFieldParser (asDbRootField . QDBAggregation) $ selectFunctionAggregate function aggName (Just aggDesc) perms
]
functionSelectExpParsers <- for functions \function -> runMaybeT $ do
let targetTable = _fiReturnType function
functionName = _fiName function
selectPerms <- lift $ tableSelectPermissions targetTable
perms <- hoistMaybe selectPerms
when (functionPermsCtx == FunctionPermissionsManual) $
-- see Note [Function Permissions]
guard $ roleName == adminRoleName || roleName `elem` (_fiPermissions function)
displayName <- functionGraphQLName @b functionName `onLeft` throwError
let functionDesc = G.Description $ "execute function " <> functionName <<> " which returns " <>> targetTable
aggName = displayName <> $$(G.litName "_aggregate")
aggDesc = G.Description $ "execute function " <> functionName <<> " and query aggregates on result of table type " <>> targetTable
catMaybes <$> sequenceA
[ requiredFieldParser (asDbRootField . QDBSimple) $ lift $ selectFunction function displayName (Just functionDesc) perms
, mapMaybeFieldParser (asDbRootField . QDBAggregation) $ lift $ selectFunctionAggregate function aggName (Just aggDesc) perms
]
pure $ (concat . catMaybes) (tableSelectExpParsers <> functionSelectExpParsers)
where
asDbRootField =
let pgExecCtx = PG._pscExecCtx sourceConfig
in RFDB sourceName pgExecCtx
mapMaybeFieldParser :: (a -> a') -> m (Maybe (P.FieldParser n a)) -> m (Maybe (P.FieldParser n a'))
mapMaybeFieldParser f = fmap $ fmap $ fmap f
requiredFieldParser
:: (Functor n, Functor m)=> (a -> b) -> m (P.FieldParser n a) -> m (Maybe (P.FieldParser n b))
requiredFieldParser f = fmap $ Just . fmap f
mapMaybeFieldParser
:: (Functor n, Functor m)
=> (a -> b)
-> m (Maybe (P.FieldParser n a))
-> m (Maybe (P.FieldParser n b))
mapMaybeFieldParser f = fmap $ fmap $ fmap f
-- | Includes remote schema fields and actions
buildActionQueryFields
@ -536,8 +543,8 @@ buildRelayPostgresQueryFields sourceName sourceConfig allTables (takeExposedAs F
lift $ selectTableConnection table fieldName fieldDesc pkeyColumns selectPerms
functionConnectionFields <- for queryFunctions $ \function -> runMaybeT do
let returnTable = fiReturnType function
functionName = fiName function
let returnTable = _fiReturnType function
functionName = _fiName function
pkeyColumns <- MaybeT $ (^? tiCoreInfo.tciPrimaryKey._Just.pkColumns)
<$> askTableInfo returnTable
selectPerms <- MaybeT $ tableSelectPermissions returnTable
@ -762,24 +769,34 @@ buildMutationParser
-> m (Maybe (Parser 'Output n (OMap.InsOrdHashMap G.Name (MutationRootField (UnpreparedValue 'Postgres)))))
buildMutationParser allRemotes allActions nonObjectCustomTypes
(takeExposedAs FEAMutation fst -> mutationFunctions) pgMutationFields = do
roleName <- askRoleName
functionPermsCtx <- asks $ qcFunctionPermsContext . getter
-- NOTE: this is basically copied from functionSelectExpParsers body
functionMutationExpParsers <- for mutationFunctions \(function@FunctionInfo{..}, (sourceName, sourceConfig)) -> do
selectPerms <- tableSelectPermissions fiReturnType
for selectPerms \perms -> do
displayName <- PG.qualifiedObjectToName fiName
let functionDesc = G.Description $
"execute VOLATILE function " <> fiName <<> " which returns " <>> fiReturnType
asDbRootField =
let pgExecCtx = PG._pscExecCtx sourceConfig
in RFDB sourceName pgExecCtx
functionMutationExpParsers <-
case functionPermsCtx of
-- when function permissions are inferred, we don't expose the
-- mutation functions. See Note [Function Permissions]
FunctionPermissionsInferred -> pure []
FunctionPermissionsManual ->
for mutationFunctions \(function@FunctionInfo{..}, (sourceName, sourceConfig)) -> runMaybeT do
selectPerms <- lift $ tableSelectPermissions _fiReturnType
-- A function exposed as mutation must have a function permission
-- configured for the role. See Note [Function Permissions]
guard $ roleName == adminRoleName || roleName `elem` _fiPermissions
perms <- hoistMaybe selectPerms
displayName <- PG.qualifiedObjectToName _fiName
let functionDesc = G.Description $
"execute VOLATILE function " <> _fiName <<> " which returns " <>> _fiReturnType
asDbRootField =
let pgExecCtx = PG._pscExecCtx sourceConfig
in RFDB sourceName pgExecCtx
catMaybes <$> sequenceA
[ requiredFieldParser (asDbRootField . MDBFunction) $
selectFunction function displayName (Just functionDesc) perms
-- FWIW: The equivalent of this is possible for mutations; do we want that?:
-- , mapMaybeFieldParser (asDbRootField . QDBAggregation) $ selectFunctionAggregate function aggName (Just aggDesc) perms
]
catMaybes <$> sequenceA
[ requiredFieldParser (asDbRootField . MDBFunction) $
lift $ selectFunction function displayName (Just functionDesc) perms
-- FWIW: The equivalent of this is possible for mutations; do we want that?:
-- , mapMaybeFieldParser (asDbRootField . QDBAggregation) $ selectFunctionAggregate function aggName (Just aggDesc) perms
]
actionParsers <- for allActions $ \actionInfo ->
case _adType (_aiDefinition actionInfo) of
@ -804,7 +821,7 @@ buildMutationParser allRemotes allActions nonObjectCustomTypes
-- local helpers
takeExposedAs :: FunctionExposedAs -> (a -> FunctionInfo b) -> [a] -> [a]
takeExposedAs x f = filter ((== x) . fiExposedAs . f)
takeExposedAs x f = filter ((== x) . _fiExposedAs . f)
subscriptionRoot :: G.Name
subscriptionRoot = $$(G.litName "subscription_root")

View File

@ -33,6 +33,7 @@ data QueryContext =
{ qcStringifyNum :: !Bool
, qcQueryType :: !ET.GraphQLQueryType
, qcRemoteRelationshipContext :: !(HashMap RemoteSchemaName (IntrospectionResult, ParsedIntrospection))
, qcFunctionPermsContext :: !FunctionPermissionsCtx
}
textToName :: MonadError QErr m => Text -> m G.Name
@ -129,4 +130,4 @@ takeValidTables = Map.filterWithKey graphQLTableFilter . Map.filter tableFilter
takeValidFunctions :: forall b. FunctionCache b -> [FunctionInfo b]
takeValidFunctions = Map.elems . Map.filter functionFilter
where
functionFilter = not . isSystemDefined . fiSystemDefined
functionFilter = not . isSystemDefined . _fiSystemDefined

View File

@ -464,7 +464,7 @@ selectFunction
-> m (FieldParser n (SelectExp b))
selectFunction function fieldName description selectPermissions = do
stringifyNum <- asks $ qcStringifyNum . getter
let table = fiReturnType function
let table = _fiReturnType function
tableArgsParser <- tableArgs table selectPermissions
functionArgsParser <- customSQLFunctionArgs function
selectionSetParser <- tableSelectionList table selectPermissions
@ -472,7 +472,7 @@ selectFunction function fieldName description selectPermissions = do
pure $ P.subselection fieldName description argsParser selectionSetParser
<&> \((funcArgs, tableArgs'), fields) -> IR.AnnSelectG
{ IR._asnFields = fields
, IR._asnFrom = IR.FromFunction (fiName function) funcArgs Nothing
, IR._asnFrom = IR.FromFunction (_fiName function) funcArgs Nothing
, IR._asnPerm = tablePermissionsInfo selectPermissions
, IR._asnArgs = tableArgs'
, IR._asnStrfyNum = stringifyNum
@ -492,7 +492,7 @@ selectFunctionAggregate
-> SelPermInfo b -- ^ select permissions of the target table
-> m (Maybe (FieldParser n (AggSelectExp b)))
selectFunctionAggregate function fieldName description selectPermissions = runMaybeT do
let table = fiReturnType function
let table = _fiReturnType function
stringifyNum <- asks $ qcStringifyNum . getter
guard $ spiAllowAgg selectPermissions
tableGQLName <- getTableGQLName @b table
@ -511,7 +511,7 @@ selectFunctionAggregate function fieldName description selectPermissions = runMa
pure $ P.subselection fieldName description argsParser aggregationParser
<&> \((funcArgs, tableArgs'), fields) -> IR.AnnSelectG
{ IR._asnFields = fields
, IR._asnFrom = IR.FromFunction (fiName function) funcArgs Nothing
, IR._asnFrom = IR.FromFunction (_fiName function) funcArgs Nothing
, IR._asnPerm = tablePermissionsInfo selectPermissions
, IR._asnArgs = tableArgs'
, IR._asnStrfyNum = stringifyNum
@ -532,7 +532,7 @@ selectFunctionConnection
-> m (FieldParser n (ConnectionSelectExp 'Postgres))
selectFunctionConnection function fieldName description pkeyColumns selectPermissions = do
stringifyNum <- asks $ qcStringifyNum . getter
let table = fiReturnType function
let table = _fiReturnType function
tableConnectionArgsParser <- tableConnectionArgs pkeyColumns table selectPermissions
functionArgsParser <- customSQLFunctionArgs function
selectionSetParser <- tableConnectionSelectionSet table selectPermissions
@ -544,7 +544,7 @@ selectFunctionConnection function fieldName description pkeyColumns selectPermis
, IR._csSlice = slice
, IR._csSelect = IR.AnnSelectG
{ IR._asnFields = fields
, IR._asnFrom = IR.FromFunction (fiName function) funcArgs Nothing
, IR._asnFrom = IR.FromFunction (_fiName function) funcArgs Nothing
, IR._asnPerm = tablePermissionsInfo selectPermissions
, IR._asnArgs = args
, IR._asnStrfyNum = stringifyNum
@ -1130,7 +1130,7 @@ customSQLFunctionArgs
:: (BackendSchema b, MonadSchema n m, MonadTableInfo r m)
=> FunctionInfo b
-> m (InputFieldsParser n (IR.FunctionArgsExpTableRow b (UnpreparedValue b)))
customSQLFunctionArgs FunctionInfo{..} = functionArgs fiName fiInputArgs
customSQLFunctionArgs FunctionInfo{..} = functionArgs _fiName _fiInputArgs
-- | Parses the arguments to the underlying sql function of a computed field or
-- a custom function. All arguments to the underlying sql function are parsed

View File

@ -55,7 +55,7 @@ pgIdenTrigger op trn = pgFmtIdentifier . qualifyTriggerName op $ triggerNameToTx
qualifyTriggerName op' trn' = "notify_hasura_" <> trn' <> "_" <> tshow op'
mkAllTriggersQ
:: (MonadTx m, HasSQLGenCtx m)
:: (MonadTx m, HasServerConfigCtx m)
=> TriggerName
-> QualifiedTable
-> [ColumnInfo 'Postgres]
@ -67,7 +67,7 @@ mkAllTriggersQ trn qt allCols fullspec = do
onJust (tdDelete fullspec) (mkTriggerQ trn qt allCols DELETE)
mkTriggerQ
:: (MonadTx m, HasSQLGenCtx m)
:: (MonadTx m, HasServerConfigCtx m)
=> TriggerName
-> QualifiedTable
-> [ColumnInfo 'Postgres]
@ -75,7 +75,7 @@ mkTriggerQ
-> SubscribeOpSpec
-> m ()
mkTriggerQ trn qt@(QualifiedObject schema table) allCols op (SubscribeOpSpec columns payload) = do
strfyNum <- stringifyNum <$> askSQLGenCtx
strfyNum <- stringifyNum . _sccSQLGenCtx <$> askServerConfigCtx
liftTx $ Q.multiQE defaultTxErrorHandler $ Q.fromText . TL.toStrict $
let payloadColumns = fromMaybe SubCStar payload
mkQId opVar colInfo = toJSONableExp strfyNum (pgiType colInfo) False $
@ -255,7 +255,7 @@ runCreateEventTriggerQuery q = do
-- transaction as soon as after @'runCreateEventTriggerQuery' is called and
-- in building schema cache.
createPostgresTableEventTrigger
:: (MonadTx m, HasSQLGenCtx m)
:: (MonadTx m, HasServerConfigCtx m)
=> QualifiedTable
-> [ColumnInfo 'Postgres]
-> TriggerName

View File

@ -20,7 +20,7 @@ import qualified Data.HashMap.Strict.InsOrd as OMap
import qualified Data.HashSet as HS
import qualified Data.List as L
import Control.Lens ((^?))
import Control.Lens ((.~), (^?))
import Data.Aeson
import Hasura.Metadata.Class
@ -108,11 +108,38 @@ runReplaceMetadata replaceMetadata = do
pure successMsg
runExportMetadata
:: (MetadataM m)
:: forall m . ( QErrM m, MetadataM m, HasServerConfigCtx m)
=> ExportMetadata -> m EncJSON
runExportMetadata _ =
AO.toEncJSON . metadataToOrdJSON <$> getMetadata
runExportMetadata _ = do
functionPermsCtx <- _sccFunctionPermsCtx <$> askServerConfigCtx
metadata <- getMetadata
exportMetadata <- processFunctionPermissions functionPermsCtx metadata
pure $ AO.toEncJSON . metadataToOrdJSON $ exportMetadata
where
-- | when FunctionPermissionsCtx is set to `FunctionPermissionsInferred`
-- we don't export the function permissions to the exported metadata
-- Note: Please **do not** make this function public as this is only meant
-- to be used while exporting metadata i.e. we don't intend on deleting
-- any function permissions that may exist in the DB, we simply hide it
-- from the user.
processFunctionPermissions :: FunctionPermissionsCtx -> Metadata -> m Metadata
processFunctionPermissions FunctionPermissionsManual metadata = pure metadata
processFunctionPermissions FunctionPermissionsInferred metadata =
let sources = OMap.keys $ _metaSources metadata
in foldrM clearFunctionPermission metadata sources
where
clearFunctionPermission sourceName accumulatedMetadata = do
let sourceFunctions =
OMap.keys . _smFunctions <$> OMap.lookup sourceName (_metaSources accumulatedMetadata)
functions <- onNothing sourceFunctions (throw500 "unexpected: runExportMetadata - source not found")
pure $ foldr
(\functionName md ->
((metaSources.ix sourceName.smFunctions.ix functionName.fmPermissions .~ mempty) md))
accumulatedMetadata
functions
runReloadMetadata :: (QErrM m, CacheRWM m, MetadataM m) => ReloadMetadata -> m EncJSON
runReloadMetadata (ReloadMetadata reloadRemoteSchemas reloadSources) = do
@ -177,7 +204,8 @@ purgeMetadataObj = \case
MTOTrigger trn -> dropEventTriggerInMetadata trn
MTOComputedField ccn -> dropComputedFieldInMetadata ccn
MTORemoteRelationship rn -> dropRemoteRelationshipInMetadata rn
SMOFunction qf -> dropFunctionInMetadata source qf
SMOFunction qf -> dropFunctionInMetadata source qf
SMOFunctionPermission qf rn -> dropFunctionPermissionInMetadata source qf rn
MORemoteSchema rsn -> dropRemoteSchemaInMetadata rsn
MORemoteSchemaPermissions rsName role -> dropRemoteSchemaPermissionInMetadata rsName role
MOCustomTypes -> clearCustomTypesInMetadata

View File

@ -64,6 +64,9 @@ instance Arbitrary MetadataVersion where
instance Arbitrary FunctionMetadata where
arbitrary = genericArbitrary
instance Arbitrary FunctionPermissionMetadata where
arbitrary = genericArbitrary
instance Arbitrary PostgresPoolSettings where
arbitrary = genericArbitrary

View File

@ -56,13 +56,13 @@ runAddRemoteSchema env q@(AddRemoteSchemaQuery name defn comment) = do
runAddRemoteSchemaPermissions
:: ( QErrM m
, CacheRWM m
, HasRemoteSchemaPermsCtx m
, HasServerConfigCtx m
, MetadataM m
)
=> AddRemoteSchemaPermissions
-> m EncJSON
runAddRemoteSchemaPermissions q = do
remoteSchemaPermsCtx <- askRemoteSchemaPermsCtx
remoteSchemaPermsCtx <- _sccRemoteSchemaPermsCtx <$> askServerConfigCtx
unless (remoteSchemaPermsCtx == RemoteSchemaPermsEnabled) $ do
throw400 ConstraintViolation
$ "remote schema permissions can only be added when "

View File

@ -105,7 +105,7 @@ isSchemaCacheBuildRequiredRunSQL RunSQL {..} =
{ TDFA.captureGroups = False }
"\\balter\\b|\\bdrop\\b|\\breplace\\b|\\bcreate function\\b|\\bcomment on\\b")
runRunSQL :: (MonadIO m, MonadBaseControl IO m, MonadError QErr m, CacheRWM m, HasSQLGenCtx m, MetadataM m)
runRunSQL :: (MonadIO m, MonadBaseControl IO m, MonadError QErr m, CacheRWM m, HasServerConfigCtx m, MetadataM m)
=> RunSQL -> m EncJSON
runRunSQL q@RunSQL {..}
-- see Note [Checking metadata consistency in run_sql]

View File

@ -92,8 +92,8 @@ newtype CacheRWT m a
= CacheRWT (StateT (RebuildableSchemaCache, CacheInvalidations) m a)
deriving
( Functor, Applicative, Monad, MonadIO, MonadUnique, MonadReader r, MonadError e, MonadTx
, UserInfoM, HasHttpManagerM, HasSQLGenCtx, HasSystemDefined, MonadMetadataStorage
, MonadMetadataStorageQueryAPI, HasRemoteSchemaPermsCtx, Tracing.MonadTrace)
, UserInfoM, HasHttpManagerM, HasSystemDefined, MonadMetadataStorage
, MonadMetadataStorageQueryAPI, Tracing.MonadTrace, HasServerConfigCtx)
deriving instance (MonadBase IO m) => MonadBase IO (CacheRWT m)
deriving instance (MonadBaseControl IO m) => MonadBaseControl IO (CacheRWT m)
@ -110,8 +110,8 @@ instance MonadTrans CacheRWT where
instance (Monad m) => CacheRM (CacheRWT m) where
askSchemaCache = CacheRWT $ gets (lastBuiltSchemaCache . (^. _1))
instance (MonadIO m, MonadError QErr m, HasHttpManagerM m, HasSQLGenCtx m
, HasRemoteSchemaPermsCtx m, MonadResolveSource m) => CacheRWM (CacheRWT m) where
instance (MonadIO m, MonadError QErr m, HasHttpManagerM m
, MonadResolveSource m, HasServerConfigCtx m) => CacheRWM (CacheRWT m) where
buildSchemaCacheWithOptions buildReason invalidations metadata = CacheRWT do
(RebuildableSchemaCache _ invalidationKeys rule, oldInvalidations) <- get
let newInvalidationKeys = invalidateKeys invalidations invalidationKeys
@ -134,7 +134,8 @@ buildSchemaCacheRule
-- what we want!
:: ( HasVersion, ArrowChoice arr, Inc.ArrowDistribute arr, Inc.ArrowCache m arr
, MonadIO m, MonadUnique m, MonadBaseControl IO m, MonadError QErr m
, MonadReader BuildReason m, HasHttpManagerM m, HasSQLGenCtx m , HasRemoteSchemaPermsCtx m, MonadResolveSource m)
, MonadReader BuildReason m, HasHttpManagerM m, MonadResolveSource m
, HasServerConfigCtx m)
=> Env.Environment
-> (Metadata, InvalidationKeys) `arr` SchemaCache
buildSchemaCacheRule env = proc (metadata, invalidationKeys) -> do
@ -216,7 +217,7 @@ buildSchemaCacheRule env = proc (metadata, invalidationKeys) -> do
buildSource
:: ( ArrowChoice arr, Inc.ArrowDistribute arr, Inc.ArrowCache m arr
, ArrowWriter (Seq CollectedInfo) arr, MonadBaseControl IO m
, HasSQLGenCtx m, MonadIO m, MonadError QErr m, MonadReader BuildReason m)
, HasServerConfigCtx m, MonadIO m, MonadError QErr m, MonadReader BuildReason m)
=> ( SourceMetadata
, SourceConfig 'Postgres
, DBTablesMetadata 'Postgres
@ -259,7 +260,7 @@ buildSchemaCacheRule env = proc (metadata, invalidationKeys) -> do
-- sql functions
functionCache <- (mapFromL _fmFunction (OMap.elems functions) >- returnA)
>-> (| Inc.keyed (\_ (FunctionMetadata qf config) -> do
>-> (| Inc.keyed (\_ (FunctionMetadata qf config funcPermissions) -> do
let systemDefined = SystemDefined False
definition = toJSON $ TrackFunction qf
metadataObject = MetadataObject (MOSourceObjId source $ SMOFunction qf) definition
@ -269,7 +270,7 @@ buildSchemaCacheRule env = proc (metadata, invalidationKeys) -> do
(| modifyErrA (do
let funcDefs = fromMaybe [] $ M.lookup qf pgFunctions
rawfi <- bindErrorA -< handleMultipleFunctions qf funcDefs
(fi, dep) <- bindErrorA -< mkFunctionInfo source qf systemDefined config rawfi
(fi, dep) <- bindErrorA -< mkFunctionInfo source qf systemDefined config funcPermissions rawfi
recordDependencies -< (metadataObject, schemaObject, [dep])
returnA -< fi)
|) addFunctionContext)
@ -282,7 +283,7 @@ buildSchemaCacheRule env = proc (metadata, invalidationKeys) -> do
:: ( ArrowChoice arr, Inc.ArrowDistribute arr, Inc.ArrowCache m arr
, ArrowWriter (Seq CollectedInfo) arr, MonadIO m, MonadUnique m, MonadError QErr m
, MonadReader BuildReason m, MonadBaseControl IO m
, HasHttpManagerM m, HasSQLGenCtx m, MonadResolveSource m)
, HasHttpManagerM m, HasServerConfigCtx m, MonadResolveSource m)
=> (Metadata, Inc.Dependency InvalidationKeys) `arr` BuildOutputs 'Postgres
buildAndCollectInfo = proc (metadata, invalidationKeys) -> do
let Metadata sources remoteSchemas collections allowlists
@ -466,7 +467,7 @@ buildSchemaCacheRule env = proc (metadata, invalidationKeys) -> do
buildTableEventTriggers
:: ( ArrowChoice arr, Inc.ArrowDistribute arr, ArrowWriter (Seq CollectedInfo) arr
, Inc.ArrowCache m arr, MonadIO m, MonadError QErr m, MonadBaseControl IO m
, MonadReader BuildReason m, HasSQLGenCtx m)
, MonadReader BuildReason m, HasServerConfigCtx m)
=> ( SourceName, SourceConfig 'Postgres, TableCoreInfo 'Postgres
, [EventTriggerConf], Inc.Dependency Inc.InvalidationKey
) `arr` EventTriggerInfoMap
@ -562,7 +563,7 @@ buildSchemaCacheRule env = proc (metadata, invalidationKeys) -> do
-- result. If it did, it checks to ensure the changes do not violate any integrity constraints, and
-- if not, incorporates them into the schema cache.
withMetadataCheck
:: (MonadIO m, MonadBaseControl IO m, MonadError QErr m, CacheRWM m, HasSQLGenCtx m, MetadataM m)
:: (MonadIO m, MonadBaseControl IO m, MonadError QErr m, CacheRWM m, HasServerConfigCtx m, MetadataM m)
=> SourceName -> Bool -> Q.TxAccess -> LazyTxT QErr m a -> m a
withMetadataCheck source cascade txAccess action = do
SourceInfo _ preActionTables preActionFunctions sourceConfig <- askSourceInfo source

View File

@ -117,10 +117,9 @@ $(makeLenses ''BuildOutputs)
-- | Parameters required for schema cache build
data CacheBuildParams
= CacheBuildParams
{ _cbpManager :: !HTTP.Manager
, _cbpSqlGenCtx :: !SQLGenCtx
, _cbpRemoteSchemaPermsCtx :: !RemoteSchemaPermsCtx
, _cbpSourceResolver :: !SourceResolver
{ _cbpManager :: !HTTP.Manager
, _cbpSourceResolver :: !SourceResolver
, _cbpServerConfigCtx :: !ServerConfigCtx
}
-- | The monad in which @'RebuildableSchemaCache' is being run
@ -138,11 +137,8 @@ newtype CacheBuild a
instance HTTP.HasHttpManagerM CacheBuild where
askHttpManager = asks _cbpManager
instance HasSQLGenCtx CacheBuild where
askSQLGenCtx = asks _cbpSqlGenCtx
instance HasRemoteSchemaPermsCtx CacheBuild where
askRemoteSchemaPermsCtx = asks _cbpRemoteSchemaPermsCtx
instance HasServerConfigCtx CacheBuild where
askServerConfigCtx = asks _cbpServerConfigCtx
instance MonadResolveSource CacheBuild where
getSourceResolver = asks _cbpSourceResolver
@ -160,17 +156,15 @@ runCacheBuildM
:: ( MonadIO m
, MonadError QErr m
, HTTP.HasHttpManagerM m
, HasSQLGenCtx m
, HasRemoteSchemaPermsCtx m
, HasServerConfigCtx m
, MonadResolveSource m
)
=> CacheBuild a -> m a
runCacheBuildM m = do
params <- CacheBuildParams
<$> HTTP.askHttpManager
<*> askSQLGenCtx
<*> askRemoteSchemaPermsCtx
<*> getSourceResolver
<*> askServerConfigCtx
runCacheBuild params m
data RebuildableSchemaCache

View File

@ -156,6 +156,8 @@ deleteMetadataObject = \case
deleteObjFn = \case
SMOTable name -> siTables %~ M.delete name
SMOFunction name -> siFunctions %~ M.delete name
SMOFunctionPermission functionName role ->
siFunctions.ix functionName.fiPermissions %~ HS.delete role
SMOTableObj tableName tableObjectId -> siTables.ix tableName %~ case tableObjectId of
MTORel name _ -> tiCoreInfo.tciFieldInfoMap %~ M.delete (fromRel name)
MTOComputedField name -> tiCoreInfo.tciFieldInfoMap %~ M.delete (fromComputedField name)

View File

@ -7,7 +7,9 @@ module Hasura.RQL.DDL.Schema.Function where
import Hasura.Prelude
import qualified Control.Monad.Validate as MV
import qualified Data.HashMap.Strict as Map
import qualified Data.HashMap.Strict.InsOrd as OMap
import qualified Data.HashSet as Set
import qualified Data.Sequence as Seq
import qualified Data.Text as T
import qualified Database.PG.Query as Q
@ -24,6 +26,7 @@ import Hasura.EncJSON
import Hasura.RQL.Types
import Hasura.Server.Utils (englishList, makeReasonMessage)
import Hasura.Session
mkFunctionArgs :: Int -> [QualifiedPGType] -> [FunctionArgName] -> [FunctionArg 'Postgres]
mkFunctionArgs defArgsNo tys argNames =
@ -69,9 +72,10 @@ mkFunctionInfo
-> QualifiedFunction
-> SystemDefined
-> FunctionConfig
-> [FunctionPermissionMetadata]
-> RawFunctionInfo
-> m (FunctionInfo 'Postgres, SchemaDependency)
mkFunctionInfo source qf systemDefined FunctionConfig{..} rawFuncInfo =
mkFunctionInfo source qf systemDefined FunctionConfig{..} permissions rawFuncInfo =
either (throw400 NotSupported . showErrors) pure
=<< MV.runValidateT validateFunction
where
@ -113,8 +117,10 @@ mkFunctionInfo source qf systemDefined FunctionConfig{..} rawFuncInfo =
inputArguments <- makeInputArguments
let retTable = typeToTable returnType
functionInfo =
FunctionInfo qf systemDefined funVol exposeAs inputArguments retTable descM (Set.fromList $ _fpmRole <$> permissions)
pure ( FunctionInfo qf systemDefined funVol exposeAs inputArguments retTable descM
pure ( functionInfo
, SchemaDependency (SOSourceObj source $ SOITable retTable) DRTable
)
@ -183,7 +189,7 @@ trackFunctionP2 sourceName qf config = do
buildSchemaCacheFor (MOSourceObjId sourceName $ SMOFunction qf)
$ MetadataModifier
$ metaSources.ix sourceName.smFunctions
%~ OMap.insert qf (FunctionMetadata qf config)
%~ OMap.insert qf (FunctionMetadata qf config mempty)
pure successMsg
handleMultipleFunctions :: (QErrM m) => QualifiedFunction -> [a] -> m a
@ -240,14 +246,19 @@ instance FromJSON UnTrackFunction where
UnTrackFunction <$> o .: "table"
<*> o .:? "source" .!= defaultSource
askPGFunctionInfo
:: (CacheRM m, MonadError QErr m)
=> SourceName -> QualifiedFunction -> m (FunctionInfo 'Postgres)
askPGFunctionInfo source functionName = do
sourceCache <- scPostgres <$> askSchemaCache
unsafeFunctionInfo @'Postgres source functionName sourceCache
`onNothing` throw400 NotExists ("function " <> functionName <<> " not found in the cache")
runUntrackFunc
:: (CacheRWM m, MonadError QErr m, MetadataM m)
=> UnTrackFunction -> m EncJSON
runUntrackFunc (UnTrackFunction functionName sourceName) = do
schemaCache <- askSchemaCache
unsafeFunctionInfo @'Postgres sourceName functionName (scPostgres schemaCache)
`onNothing` throw400 NotExists ("function not found in cache " <>> functionName)
-- Delete function from metadata
void $ askPGFunctionInfo sourceName functionName
withNewInconsistentObjsCheck
$ buildSchemaCache
$ dropFunctionInMetadata defaultSource functionName
@ -256,3 +267,93 @@ runUntrackFunc (UnTrackFunction functionName sourceName) = do
dropFunctionInMetadata :: SourceName -> QualifiedFunction -> MetadataModifier
dropFunctionInMetadata source function = MetadataModifier $
metaSources.ix source.smFunctions %~ OMap.delete function
{- Note [Function Permissions]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Before we started supporting tracking volatile functions, permissions
for a function was inferred from the target table of the function.
The rationale behind this is that a stable/immutable function does not
modify the database and the data returned by the function is filtered using
the permissions that are specified precisely for that data.
Now consider mutable/volatile functions, we can't automatically infer whether or
not these functions should be exposed for the sole reason that they can modify
the database. This necessitates a permission system for functions.
So, we introduce a new API `pg_create_function_permission` which will
explicitly grant permission to a function to a role. For creating a
function permission, the role must have select permissions configured
for the target table.
Since, this is a breaking change, we enable it only when the graphql-engine
is started with
`--infer-function-permissions`/HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS set
to false (by default, it's set to true).
-}
data CreateFunctionPermission
= CreateFunctionPermission
{ _afpFunction :: !QualifiedFunction
, _afpSource :: !SourceName
, _afpRole :: !RoleName
} deriving (Show, Eq)
$(deriveToJSON hasuraJSON ''CreateFunctionPermission)
instance FromJSON CreateFunctionPermission where
parseJSON v =
flip (withObject "CreateFunctionPermission") v $ \o ->
CreateFunctionPermission
<$> o .: "function"
<*> o .:? "source" .!= defaultSource
<*> o .: "role"
runCreateFunctionPermission
:: ( CacheRWM m
, MonadError QErr m
, MetadataM m
, HasServerConfigCtx m
)
=> CreateFunctionPermission
-> m EncJSON
runCreateFunctionPermission (CreateFunctionPermission functionName source role) = do
functionPermsCtx <- _sccFunctionPermsCtx <$> askServerConfigCtx
unless (functionPermsCtx == FunctionPermissionsManual) $
throw400 NotSupported "function permission can only be created when inferring of function permissions is disabled"
sourceCache <- scPostgres <$> askSchemaCache
functionInfo <- askPGFunctionInfo source functionName
when (role `elem` _fiPermissions functionInfo) $
throw400 AlreadyExists $
"permission of role "
<> role <<> " already exists for function " <> functionName <<> " in source: " <>> source
functionTableInfo <-
unsafeTableInfo @'Postgres source (_fiReturnType functionInfo) sourceCache
`onNothing` throw400 NotExists ("function's return table " <> (_fiReturnType functionInfo) <<> " not found in the cache")
unless (role `Map.member` _tiRolePermInfoMap functionTableInfo) $
throw400 NotSupported $
"function permission can only be added when the function's return table "
<> _fiReturnType functionInfo <<> " has select permission configured for role: " <>> role
buildSchemaCacheFor (MOSourceObjId source $ SMOFunctionPermission functionName role)
$ MetadataModifier
$ metaSources.ix source.smFunctions.ix functionName.fmPermissions
%~ (:) (FunctionPermissionMetadata role)
pure successMsg
dropFunctionPermissionInMetadata :: SourceName -> QualifiedFunction -> RoleName -> MetadataModifier
dropFunctionPermissionInMetadata source function role = MetadataModifier $
metaSources.ix source.smFunctions.ix function.fmPermissions %~ filter ((/=) role . _fpmRole)
type DropFunctionPermission = CreateFunctionPermission
runDropFunctionPermission
:: ( CacheRWM m
, MonadError QErr m
, MetadataM m
)
=> DropFunctionPermission
-> m EncJSON
runDropFunctionPermission (CreateFunctionPermission functionName source role) = do
functionInfo <- askPGFunctionInfo source functionName
unless (role `elem` _fiPermissions functionInfo) $
throw400 NotExists $
"permission of role "
<> role <<> " does not exist for function " <> functionName <<> " in source: " <>> source
buildSchemaCacheFor (MOSourceObjId source $ SMOFunctionPermission functionName role)
$ dropFunctionPermissionInMetadata source functionName role
pure successMsg

View File

@ -69,7 +69,7 @@ saveMetadataToHdbTables (MetadataNoSources tables functions schemas collections
-- sql functions
withPathK "functions" $ indexedForM_ functions $
\(FunctionMetadata function config) -> addFunctionToCatalog function config
\(FunctionMetadata function config _) -> addFunctionToCatalog function config
-- query collections
systemDefined <- askSystemDefined
@ -407,7 +407,10 @@ fetchMetadataFromHdbTables = liftTx do
|] () False
pure $ oMapFromL _fmFunction $
flip map l $ \(sn, fn, Q.AltJ config) ->
FunctionMetadata (QualifiedObject sn fn) 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

View File

@ -93,7 +93,7 @@ validateDeleteQ query = do
runDelete
:: ( HasVersion, QErrM m, UserInfoM m, CacheRM m
, HasSQLGenCtx m, MonadIO m
, HasServerConfigCtx m, MonadIO m
, Tracing.MonadTrace m, MonadBaseControl IO m
)
=> Env.Environment
@ -101,7 +101,7 @@ runDelete
-> m EncJSON
runDelete env q = do
sourceConfig <- askSourceConfig (doSource q)
strfyNum <- stringifyNum <$> askSQLGenCtx
strfyNum <- stringifyNum . _sccSQLGenCtx <$> askServerConfigCtx
validateDeleteQ q
>>= runQueryLazyTx (_pscExecCtx sourceConfig) Q.ReadWrite
. execDeleteQuery env strfyNum Nothing

View File

@ -208,7 +208,7 @@ convInsQ query = do
runInsert
:: ( HasVersion, QErrM m, UserInfoM m
, CacheRM m, HasSQLGenCtx m
, CacheRM m, HasServerConfigCtx m
, MonadIO m, Tracing.MonadTrace m
, MonadBaseControl IO m
)
@ -216,7 +216,7 @@ runInsert
runInsert env q = do
sourceConfig <- askSourceConfig (iqSource q)
res <- convInsQ q
strfyNum <- stringifyNum <$> askSQLGenCtx
strfyNum <- stringifyNum . _sccSQLGenCtx <$> askServerConfigCtx
runQueryLazyTx (_pscExecCtx sourceConfig) Q.ReadWrite $
execInsertQuery env strfyNum Nothing res

View File

@ -23,15 +23,15 @@ import Hasura.Backends.Postgres.SQL.Value
import Hasura.Backends.Postgres.Translate.BoolExp
import Hasura.Backends.Postgres.Translate.Column
import Hasura.RQL.Types
import Hasura.Session
import Hasura.SQL.Types
import Hasura.Session
newtype DMLP1T m a
= DMLP1T { unDMLP1T :: StateT (DS.Seq Q.PrepArg) m a }
deriving ( Functor, Applicative, Monad, MonadTrans
, MonadState (DS.Seq Q.PrepArg), MonadError e
, SourceM, TableCoreInfoRM b, TableInfoRM b, CacheRM, UserInfoM, HasSQLGenCtx
, SourceM, TableCoreInfoRM b, TableInfoRM b, CacheRM, UserInfoM, HasServerConfigCtx
)
runDMLP1T :: DMLP1T m a -> m (a, DS.Seq Q.PrepArg)

View File

@ -193,7 +193,7 @@ convOrderByElem sessVarBldr (flds, spi) = \case
throw400 UnexpectedPayload (mconcat [ fldName <<> " is a remote field" ])
convSelectQ
:: (UserInfoM m, QErrM m, TableInfoRM 'Postgres m, HasSQLGenCtx m)
:: (UserInfoM m, QErrM m, TableInfoRM 'Postgres m, HasServerConfigCtx m)
=> TableName 'Postgres
-> FieldInfoMap (FieldInfo 'Postgres) -- Table information of current table
-> SelPermInfo 'Postgres -- Additional select permission info
@ -238,7 +238,7 @@ convSelectQ table fieldInfoMap selPermInfo selQ sessVarBldr prepValBldr = do
tabArgs = SelectArgs wClause annOrdByM mQueryLimit
(S.intToSQLExp <$> mQueryOffset) Nothing
strfyNum <- stringifyNum <$> askSQLGenCtx
strfyNum <- stringifyNum . _sccSQLGenCtx <$> askServerConfigCtx
return $ AnnSelectG annFlds tabFrom tabPerm tabArgs strfyNum
where
@ -259,7 +259,7 @@ convExtSimple fieldInfoMap selPermInfo pgCol = do
relWhenPGErr = "relationships have to be expanded"
convExtRel
:: (UserInfoM m, QErrM m, TableInfoRM 'Postgres m, HasSQLGenCtx m)
:: (UserInfoM m, QErrM m, TableInfoRM 'Postgres m, HasServerConfigCtx m)
=> FieldInfoMap (FieldInfo 'Postgres)
-> RelName
-> Maybe RelName
@ -297,7 +297,7 @@ convExtRel fieldInfoMap relName mAlias selQ sessVarBldr prepValBldr = do
]
convSelectQuery
:: (UserInfoM m, QErrM m, TableInfoRM 'Postgres m, HasSQLGenCtx m)
:: (UserInfoM m, QErrM m, TableInfoRM 'Postgres m, HasServerConfigCtx m)
=> SessVarBldr 'Postgres m
-> (ColumnType 'Postgres -> Value -> m S.SQLExp)
-> SelectQuery
@ -318,7 +318,7 @@ selectP2 jsonAggSelect (sel, p) =
selectSQL = toSQL $ mkSQLSelect jsonAggSelect sel
phaseOne
:: (QErrM m, UserInfoM m, CacheRM m, HasSQLGenCtx m)
:: (QErrM m, UserInfoM m, CacheRM m, HasServerConfigCtx m)
=> SelectQuery -> m (AnnSimpleSel 'Postgres, DS.Seq Q.PrepArg)
phaseOne query = do
let sourceName = getSourceDMLQuery query
@ -332,7 +332,7 @@ phaseTwo =
runSelect
:: ( QErrM m, UserInfoM m, CacheRM m
, HasSQLGenCtx m, MonadIO m, MonadBaseControl IO m
, HasServerConfigCtx m, MonadIO m, MonadBaseControl IO m
, Tracing.MonadTrace m
)
=> SelectQuery -> m EncJSON

View File

@ -186,13 +186,13 @@ validateUpdateQuery query = do
runUpdate
:: ( HasVersion, QErrM m, UserInfoM m, CacheRM m
, HasSQLGenCtx m, MonadBaseControl IO m
, HasServerConfigCtx m, MonadBaseControl IO m
, MonadIO m, Tracing.MonadTrace m
)
=> Env.Environment -> UpdateQuery -> m EncJSON
runUpdate env q = do
sourceConfig <- askSourceConfig (uqSource q)
strfyNum <- stringifyNum <$> askSQLGenCtx
strfyNum <- stringifyNum . _sccSQLGenCtx <$> askServerConfigCtx
validateUpdateQuery q
>>= runQueryLazyTx (_pscExecCtx sourceConfig) Q.ReadWrite
. execUpdateQuery env strfyNum Nothing

View File

@ -2,10 +2,10 @@ module Hasura.RQL.Types
( MonadTx(..)
, SQLGenCtx(..)
, HasSQLGenCtx(..)
, RemoteSchemaPermsCtx(..)
, HasRemoteSchemaPermsCtx(..)
, ServerConfigCtx(..)
, HasServerConfigCtx(..)
, HasSystemDefined(..)
, HasSystemDefinedT
@ -110,6 +110,42 @@ askTabInfoSource
askTabInfoSource tableName = do
lookupTableInfo tableName >>= (`onNothing` throwTableDoesNotExist tableName)
data ServerConfigCtx
= ServerConfigCtx
{ _sccFunctionPermsCtx :: !FunctionPermissionsCtx
, _sccRemoteSchemaPermsCtx :: !RemoteSchemaPermsCtx
, _sccSQLGenCtx :: !SQLGenCtx
} deriving (Show, Eq)
class (Monad m) => HasServerConfigCtx m where
askServerConfigCtx :: m ServerConfigCtx
instance (HasServerConfigCtx m)
=> HasServerConfigCtx (ReaderT r m) where
askServerConfigCtx = lift askServerConfigCtx
instance (HasServerConfigCtx m)
=> HasServerConfigCtx (StateT s m) where
askServerConfigCtx = lift askServerConfigCtx
instance (Monoid w, HasServerConfigCtx m)
=> HasServerConfigCtx (WriterT w m) where
askServerConfigCtx = lift askServerConfigCtx
instance (HasServerConfigCtx m)
=> HasServerConfigCtx (TableCoreCacheRT b m) where
askServerConfigCtx = lift askServerConfigCtx
instance (HasServerConfigCtx m)
=> HasServerConfigCtx (TraceT m) where
askServerConfigCtx = lift askServerConfigCtx
instance (HasServerConfigCtx m)
=> HasServerConfigCtx (MetadataT m) where
askServerConfigCtx = lift askServerConfigCtx
instance (HasServerConfigCtx m)
=> HasServerConfigCtx (LazyTxT QErr m) where
askServerConfigCtx = lift askServerConfigCtx
instance (HasServerConfigCtx m) => HasServerConfigCtx (Q.TxET QErr m) where
askServerConfigCtx = lift askServerConfigCtx
instance (HasServerConfigCtx m) => HasServerConfigCtx (TableCacheRT b m) where
askServerConfigCtx = lift askServerConfigCtx
data RemoteSchemaPermsCtx
= RemoteSchemaPermsEnabled
| RemoteSchemaPermsDisabled
@ -127,53 +163,6 @@ instance ToJSON RemoteSchemaPermsCtx where
RemoteSchemaPermsEnabled -> "true"
RemoteSchemaPermsDisabled -> "false"
class (Monad m) => HasRemoteSchemaPermsCtx m where
askRemoteSchemaPermsCtx :: m RemoteSchemaPermsCtx
instance (HasRemoteSchemaPermsCtx m)
=> HasRemoteSchemaPermsCtx (ReaderT r m) where
askRemoteSchemaPermsCtx = lift askRemoteSchemaPermsCtx
instance (HasRemoteSchemaPermsCtx m)
=> HasRemoteSchemaPermsCtx (StateT s m) where
askRemoteSchemaPermsCtx = lift askRemoteSchemaPermsCtx
instance (Monoid w, HasRemoteSchemaPermsCtx m)
=> HasRemoteSchemaPermsCtx (WriterT w m) where
askRemoteSchemaPermsCtx = lift askRemoteSchemaPermsCtx
instance (HasRemoteSchemaPermsCtx m)
=> HasRemoteSchemaPermsCtx (TableCoreCacheRT b m) where
askRemoteSchemaPermsCtx = lift askRemoteSchemaPermsCtx
instance (HasRemoteSchemaPermsCtx m)
=> HasRemoteSchemaPermsCtx (TraceT m) where
askRemoteSchemaPermsCtx = lift askRemoteSchemaPermsCtx
instance (HasRemoteSchemaPermsCtx m)
=> HasRemoteSchemaPermsCtx (MetadataT m) where
askRemoteSchemaPermsCtx = lift askRemoteSchemaPermsCtx
instance (HasRemoteSchemaPermsCtx m)
=> HasRemoteSchemaPermsCtx (LazyTxT QErr m) where
askRemoteSchemaPermsCtx = lift askRemoteSchemaPermsCtx
class (Monad m) => HasSQLGenCtx m where
askSQLGenCtx :: m SQLGenCtx
instance (HasSQLGenCtx m) => HasSQLGenCtx (ReaderT r m) where
askSQLGenCtx = lift askSQLGenCtx
instance (HasSQLGenCtx m) => HasSQLGenCtx (StateT s m) where
askSQLGenCtx = lift askSQLGenCtx
instance (Monoid w, HasSQLGenCtx m) => HasSQLGenCtx (WriterT w m) where
askSQLGenCtx = lift askSQLGenCtx
instance (HasSQLGenCtx m) => HasSQLGenCtx (TableCoreCacheRT b m) where
askSQLGenCtx = lift askSQLGenCtx
instance (HasSQLGenCtx m) => HasSQLGenCtx (TraceT m) where
askSQLGenCtx = lift askSQLGenCtx
instance (HasSQLGenCtx m) => HasSQLGenCtx (MetadataT m) where
askSQLGenCtx = lift askSQLGenCtx
instance (HasSQLGenCtx m) => HasSQLGenCtx (Q.TxET QErr m) where
askSQLGenCtx = lift askSQLGenCtx
instance (HasSQLGenCtx m) => HasSQLGenCtx (LazyTxT QErr m) where
askSQLGenCtx = lift askSQLGenCtx
instance (HasSQLGenCtx m) => HasSQLGenCtx (TableCacheRT b m) where
askSQLGenCtx = lift askSQLGenCtx
class (Monad m) => HasSystemDefined m where
askSystemDefined :: m SystemDefined
@ -189,7 +178,7 @@ instance (HasSystemDefined m) => HasSystemDefined (TraceT m) where
newtype HasSystemDefinedT m a
= HasSystemDefinedT { unHasSystemDefinedT :: ReaderT SystemDefined m a }
deriving ( Functor, Applicative, Monad, MonadTrans, MonadIO, MonadUnique, MonadError e, MonadTx
, HasHttpManagerM, HasSQLGenCtx, SourceM, TableCoreInfoRM b, CacheRM, UserInfoM, HasRemoteSchemaPermsCtx)
, HasHttpManagerM, SourceM, TableCoreInfoRM b, CacheRM, UserInfoM, HasServerConfigCtx)
runHasSystemDefinedT :: SystemDefined -> HasSystemDefinedT m a -> m a
runHasSystemDefinedT systemDefined = flip runReaderT systemDefined . unHasSystemDefinedT

View File

@ -2,6 +2,7 @@ module Hasura.RQL.Types.Function where
import Hasura.Prelude
import qualified Data.HashSet as Set
import qualified Data.Sequence as Seq
import qualified Data.Text as T
@ -17,7 +18,7 @@ import qualified Hasura.Backends.Postgres.SQL.Types as PG
import Hasura.Incremental (Cacheable)
import Hasura.RQL.Types.Common
import Hasura.SQL.Backend
import Hasura.Session
-- | https://www.postgresql.org/docs/current/xfunc-volatility.html
data FunctionVolatility
@ -86,28 +87,31 @@ $(deriveJSON
-- | Tracked SQL function metadata. See 'mkFunctionInfo'.
data FunctionInfo (b :: BackendType)
= FunctionInfo
{ fiName :: !(FunctionName b)
, fiSystemDefined :: !SystemDefined
, fiVolatility :: !FunctionVolatility
, fiExposedAs :: !FunctionExposedAs
{ _fiName :: !(FunctionName b)
, _fiSystemDefined :: !SystemDefined
, _fiVolatility :: !FunctionVolatility
, _fiExposedAs :: !FunctionExposedAs
-- ^ In which part of the schema should this function be exposed?
--
-- See 'mkFunctionInfo' and '_fcExposedAs'.
, fiInputArgs :: !(Seq.Seq (FunctionInputArgument b))
, fiReturnType :: !(TableName b)
, _fiInputArgs :: !(Seq.Seq (FunctionInputArgument b))
, _fiReturnType :: !(TableName b)
-- ^ NOTE: when a table is created, a new composite type of the same name is
-- automatically created; so strictly speaking this field means "the function
-- returns the composite type corresponding to this table".
, fiDescription :: !(Maybe PG.PGDescription) -- FIXME: make generic
, _fiDescription :: !(Maybe PG.PGDescription) -- FIXME: make generic
, _fiPermissions :: !(Set.HashSet RoleName)
-- ^ Roles to which the function is accessible
} deriving (Generic)
deriving instance Backend b => Show (FunctionInfo b)
deriving instance Backend b => Eq (FunctionInfo b)
instance (Backend b) => ToJSON (FunctionInfo b) where
toJSON = genericToJSON hasuraJSON
$(makeLenses ''FunctionInfo)
getInputArgs :: FunctionInfo b -> Seq.Seq (FunctionArg b)
getInputArgs =
Seq.fromList . mapMaybe (^? _IAUserProvided) . toList . fiInputArgs
Seq.fromList . mapMaybe (^? _IAUserProvided) . toList . _fiInputArgs
type FunctionCache b = HashMap (FunctionName b) (FunctionInfo b) -- info of all functions
@ -172,3 +176,20 @@ instance Cacheable RawFunctionInfo
$(deriveJSON hasuraJSON ''RawFunctionInfo)
type PostgresFunctionsMetadata = HashMap PG.QualifiedFunction [RawFunctionInfo]
data FunctionPermissionsCtx
= FunctionPermissionsInferred
| FunctionPermissionsManual
deriving (Show, Eq)
instance FromJSON FunctionPermissionsCtx where
parseJSON = withText "FunctionPermissionsCtx" $ \t ->
case T.toLower t of
"true" -> pure FunctionPermissionsInferred
"false" -> pure FunctionPermissionsManual
_ -> fail "infer_function_permissions should be a boolean value"
instance ToJSON FunctionPermissionsCtx where
toJSON = \case
FunctionPermissionsInferred -> Bool True
FunctionPermissionsManual -> Bool False

View File

@ -51,6 +51,7 @@ instance Hashable TableMetadataObjId
data SourceMetadataObjId
= SMOTable !QualifiedTable
| SMOFunction !QualifiedFunction
| SMOFunctionPermission !QualifiedFunction !RoleName
| SMOTableObj !QualifiedTable !TableMetadataObjId
deriving (Show, Eq, Generic)
instance Hashable SourceMetadataObjId
@ -76,6 +77,7 @@ moiTypeName = \case
MOSourceObjId _ sourceObjId -> case sourceObjId of
SMOTable _ -> "table"
SMOFunction _ -> "function"
SMOFunctionPermission _ _ -> "function_permission"
SMOTableObj _ tableObjectId -> case tableObjectId of
MTORel _ relType -> relTypeToTxt relType <> "_relation"
MTOPerm _ permType -> permTypeToCode permType <> "_permission"
@ -96,6 +98,9 @@ moiName objectId = moiTypeName objectId <> " " <> case objectId of
MOSourceObjId source sourceObjId -> case sourceObjId of
SMOTable name -> toTxt name <> " in source " <> toTxt source
SMOFunction name -> toTxt name <> " in source " <> toTxt source
SMOFunctionPermission functionName roleName ->
toTxt roleName <> " permission for function "
<> toTxt functionName <> " in source " <> toTxt source
SMOTableObj tableName tableObjectId ->
let tableObjectName = case tableObjectId of
MTORel name _ -> toTxt name
@ -322,20 +327,30 @@ instance FromJSON TableMetadata where
, cfKey, rrKey
]
newtype FunctionPermissionMetadata
= FunctionPermissionMetadata
{ _fpmRole :: RoleName
} deriving (Show, Eq, Generic)
instance Cacheable FunctionPermissionMetadata
$(makeLenses ''FunctionPermissionMetadata)
$(deriveJSON hasuraJSON ''FunctionPermissionMetadata)
data FunctionMetadata
= FunctionMetadata
{ _fmFunction :: !QualifiedFunction
, _fmConfiguration :: !FunctionConfig
, _fmPermissions :: ![FunctionPermissionMetadata]
} deriving (Show, Eq, Generic)
instance Cacheable FunctionMetadata
$(makeLenses ''FunctionMetadata)
$(deriveToJSON hasuraJSON ''FunctionMetadata)
instance FromJSON FunctionMetadata where
parseJSON = withObject "Object" $ \o ->
parseJSON = withObject "FunctionMetadata" $ \o ->
FunctionMetadata
<$> o .: "function"
<*> o .:? "configuration" .!= emptyFunctionConfig
<*> o .:? "permissions" .!= []
type Tables = InsOrdHashMap QualifiedTable TableMetadata
type Functions = InsOrdHashMap QualifiedFunction FunctionMetadata
@ -454,7 +469,7 @@ instance FromJSON MetadataNoSources where
tables <- oMapFromL _tmTable <$> o .: "tables"
functionList <- o .:? "functions" .!= []
let functions = OM.fromList $ flip map functionList $
\function -> (function, FunctionMetadata function emptyFunctionConfig)
\function -> (function, FunctionMetadata function emptyFunctionConfig mempty)
pure (tables, functions)
MVVersion2 -> do
tables <- oMapFromL _tmTable <$> o .: "tables"
@ -652,9 +667,13 @@ metadataToOrdJSON ( Metadata
functionMetadataToOrdJSON :: FunctionMetadata -> AO.Value
functionMetadataToOrdJSON FunctionMetadata{..} =
AO.object $ [("function", AO.toOrdered _fmFunction)]
<> if _fmConfiguration == emptyFunctionConfig then []
else pure ("configuration", AO.toOrdered _fmConfiguration)
let confKeyPair =
if _fmConfiguration == emptyFunctionConfig then []
else pure ("configuration", AO.toOrdered _fmConfiguration)
permissionsKeyPair =
if (null _fmPermissions) then []
else pure ("permissions", AO.toOrdered _fmPermissions)
in AO.object $ [("function", AO.toOrdered _fmFunction)] <> confKeyPair <> permissionsKeyPair
remoteSchemaQToOrdJSON :: RemoteSchemaMetadata -> AO.Value
remoteSchemaQToOrdJSON (RemoteSchemaMetadata name definition comment permissions) =

View File

@ -24,10 +24,9 @@ import Hasura.Session
data RunCtx
= RunCtx
{ _rcUserInfo :: !UserInfo
, _rcHttpMgr :: !HTTP.Manager
, _rcSqlGenCtx :: !SQLGenCtx
, _rcRemoteSchemaPermsCtx :: !RemoteSchemaPermsCtx
{ _rcUserInfo :: !UserInfo
, _rcHttpMgr :: !HTTP.Manager
, _rcServerConfigCtx :: !ServerConfigCtx
}
newtype RunT m a
@ -54,11 +53,8 @@ instance (Monad m) => UserInfoM (RunT m) where
instance (Monad m) => HTTP.HasHttpManagerM (RunT m) where
askHttpManager = asks _rcHttpMgr
instance (Monad m) => HasSQLGenCtx (RunT m) where
askSQLGenCtx = asks _rcSqlGenCtx
instance (Monad m) => HasRemoteSchemaPermsCtx (RunT m) where
askRemoteSchemaPermsCtx = asks _rcRemoteSchemaPermsCtx
instance (Monad m) => HasServerConfigCtx (RunT m) where
askServerConfigCtx = asks _rcServerConfigCtx
instance (MonadResolveSource m) => MonadResolveSource (RunT m) where
getSourceResolver = RunT . lift . lift $ getSourceResolver

View File

@ -51,6 +51,10 @@ data RQLMetadata
| RMPgTrackFunction !TrackFunctionV2
| RMPgUntrackFunction !UnTrackFunction
-- Postgres function permissions
| RMPgCreateFunctionPermission !CreateFunctionPermission
| RMPgDropFunctionPermission !DropFunctionPermission
-- Postgres table relationships
| RMPgCreateObjectRelationship !CreateObjRel
| RMPgCreateArrayRelationship !CreateArrRel
@ -160,21 +164,19 @@ runMetadataQuery
-> InstanceId
-> UserInfo
-> HTTP.Manager
-> SQLGenCtx
-> RemoteSchemaPermsCtx
-> ServerConfigCtx
-> RebuildableSchemaCache
-> RQLMetadata
-> m (EncJSON, RebuildableSchemaCache)
runMetadataQuery env instanceId userInfo httpManager sqlGenCtx remoteSchemaPermsCtx schemaCache query = do
runMetadataQuery env instanceId userInfo httpManager serverConfigCtx schemaCache query = do
metadata <- fetchMetadata
((r, modMetadata), modSchemaCache, cacheInvalidations) <-
runMetadataQueryM env query
& runMetadataT metadata
& runCacheRWT schemaCache
& peelRun (RunCtx userInfo httpManager sqlGenCtx remoteSchemaPermsCtx)
& peelRun (RunCtx userInfo httpManager serverConfigCtx)
& runExceptT
& liftEitherM
-- set modified metadata in storage
setMetadata modMetadata
-- notify schema cache sync
@ -193,7 +195,7 @@ runMetadataQueryM
, HTTP.HasHttpManagerM m
, MetadataM m
, MonadMetadataStorageQueryAPI m
, HasRemoteSchemaPermsCtx m
, HasServerConfigCtx m
)
=> Env.Environment
-> RQLMetadata
@ -210,6 +212,9 @@ runMetadataQueryM env = withPathK "args" . \case
RMPgTrackFunction q -> runTrackFunctionV2 q
RMPgUntrackFunction q -> runUntrackFunc q
RMPgCreateFunctionPermission q -> runCreateFunctionPermission q
RMPgDropFunctionPermission q -> runDropFunctionPermission q
RMPgCreateObjectRelationship q -> runCreateRelationship ObjRel q
RMPgCreateArrayRelationship q -> runCreateRelationship ArrRel q
RMPgDropRelationship q -> runDropRel q

View File

@ -191,8 +191,9 @@ runQuery
=> Env.Environment
-> InstanceId
-> UserInfo -> RebuildableSchemaCache -> HTTP.Manager
-> SQLGenCtx -> RemoteSchemaPermsCtx -> RQLQuery -> m (EncJSON, RebuildableSchemaCache)
runQuery env instanceId userInfo sc hMgr sqlGenCtx remoteSchemaPermsCtx query = do
-> SQLGenCtx -> RemoteSchemaPermsCtx -> FunctionPermissionsCtx
-> RQLQuery -> m (EncJSON, RebuildableSchemaCache)
runQuery env instanceId userInfo sc hMgr sqlGenCtx remoteSchemaPermsCtx functionPermsCtx query = do
metadata <- fetchMetadata
result <- runQueryM env query & Tracing.interpTraceT \x -> do
(((js, tracemeta), meta), rsc, ci) <-
@ -204,7 +205,8 @@ runQuery env instanceId userInfo sc hMgr sqlGenCtx remoteSchemaPermsCtx query =
pure ((js, rsc, ci, meta), tracemeta)
withReload result
where
runCtx = RunCtx userInfo hMgr sqlGenCtx remoteSchemaPermsCtx
serverConfigCtx = ServerConfigCtx functionPermsCtx remoteSchemaPermsCtx sqlGenCtx
runCtx = RunCtx userInfo hMgr serverConfigCtx
withReload (result, updatedCache, invalidations, updatedMetadata) = do
when (queryModifiesSchemaCache query) $ do
@ -350,8 +352,8 @@ reconcileAccessModes (Just mode1) (Just mode2)
runQueryM
:: ( HasVersion, CacheRWM m, UserInfoM m
, MonadBaseControl IO m, MonadIO m, MonadUnique m
, HasHttpManagerM m, HasSQLGenCtx m
, HasRemoteSchemaPermsCtx m
, HasHttpManagerM m
, HasServerConfigCtx m
, Tracing.MonadTrace m
, MetadataM m
, MonadMetadataStorageQueryAPI m

View File

@ -66,9 +66,10 @@ runQuery
-> HTTP.Manager
-> SQLGenCtx
-> RemoteSchemaPermsCtx
-> FunctionPermissionsCtx
-> RQLQuery
-> m (EncJSON, RebuildableSchemaCache)
runQuery env instanceId userInfo schemaCache httpManager sqlGenCtx remoteSchemaPermCtx rqlQuery = do
runQuery env instanceId userInfo schemaCache httpManager sqlGenCtx remoteSchemaPermCtx functionPermsCtx rqlQuery = do
metadata <- fetchMetadata
result <- runQueryM env rqlQuery & Tracing.interpTraceT \x -> do
(((js, tracemeta), meta), rsc, ci) <-
@ -80,7 +81,8 @@ runQuery env instanceId userInfo schemaCache httpManager sqlGenCtx remoteSchemaP
pure ((js, rsc, ci, meta), tracemeta)
withReload result
where
runCtx = RunCtx userInfo httpManager sqlGenCtx remoteSchemaPermCtx
runCtx = RunCtx userInfo httpManager
$ ServerConfigCtx functionPermsCtx remoteSchemaPermCtx sqlGenCtx
withReload (result, updatedCache, invalidations, updatedMetadata) = do
when (queryModifiesSchema rqlQuery) $ do
@ -92,9 +94,9 @@ runQuery env instanceId userInfo schemaCache httpManager sqlGenCtx remoteSchemaP
queryModifiesSchema :: RQLQuery -> Bool
queryModifiesSchema = \case
RQRunSql q -> isSchemaCacheBuildRequiredRunSQL q
RQBulk l -> any queryModifiesSchema l
_ -> False
RQRunSql q -> isSchemaCacheBuildRequiredRunSQL q
RQBulk l -> any queryModifiesSchema l
_ -> False
runQueryM
:: ( HasVersion
@ -103,7 +105,7 @@ runQueryM
, MonadBaseControl IO m
, UserInfoM m
, CacheRWM m
, HasSQLGenCtx m
, HasServerConfigCtx m
, Tracing.MonadTrace m
, MetadataM m
)

View File

@ -113,6 +113,7 @@ data ServerCtx
, scResponseInternalErrorsConfig :: !ResponseInternalErrorsConfig
, scEnvironment :: !Env.Environment
, scRemoteSchemaPermsCtx :: !RemoteSchemaPermsCtx
, scFunctionPermsCtx :: !FunctionPermissionsCtx
}
data HandlerCtx
@ -391,15 +392,16 @@ v1QueryHandler query = do
return $ HttpResponse res []
where
action = do
userInfo <- asks hcUser
scRef <- asks (scCacheRef . hcServerCtx)
schemaCache <- fmap fst $ liftIO $ readIORef $ _scrCache scRef
httpMgr <- asks (scManager . hcServerCtx)
sqlGenCtx <- asks (scSQLGenCtx . hcServerCtx)
instanceId <- asks (scInstanceId . hcServerCtx)
env <- asks (scEnvironment . hcServerCtx)
userInfo <- asks hcUser
scRef <- asks (scCacheRef . hcServerCtx)
schemaCache <- fmap fst $ liftIO $ readIORef $ _scrCache scRef
httpMgr <- asks (scManager . hcServerCtx)
sqlGenCtx <- asks (scSQLGenCtx . hcServerCtx)
instanceId <- asks (scInstanceId . hcServerCtx)
env <- asks (scEnvironment . hcServerCtx)
remoteSchemaPermsCtx <- asks (scRemoteSchemaPermsCtx . hcServerCtx)
runQuery env instanceId userInfo schemaCache httpMgr sqlGenCtx remoteSchemaPermsCtx query
functionPermsCtx <- asks (scFunctionPermsCtx . hcServerCtx)
runQuery env instanceId userInfo schemaCache httpMgr sqlGenCtx remoteSchemaPermsCtx functionPermsCtx query
v1MetadataHandler
:: ( HasVersion
@ -414,17 +416,19 @@ v1MetadataHandler
=> RQLMetadata -> m (HttpResponse EncJSON)
v1MetadataHandler query = do
(liftEitherM . authorizeV1MetadataApi query) =<< ask
userInfo <- asks hcUser
scRef <- asks (scCacheRef . hcServerCtx)
schemaCache <- fmap fst $ liftIO $ readIORef $ _scrCache scRef
httpMgr <- asks (scManager . hcServerCtx)
sqlGenCtx <- asks (scSQLGenCtx . hcServerCtx)
env <- asks (scEnvironment . hcServerCtx)
instanceId <- asks (scInstanceId . hcServerCtx)
logger <- asks (scLogger . hcServerCtx)
userInfo <- asks hcUser
scRef <- asks (scCacheRef . hcServerCtx)
schemaCache <- fmap fst $ liftIO $ readIORef $ _scrCache scRef
httpMgr <- asks (scManager . hcServerCtx)
sqlGenCtx <- asks (scSQLGenCtx . hcServerCtx)
env <- asks (scEnvironment . hcServerCtx)
instanceId <- asks (scInstanceId . hcServerCtx)
logger <- asks (scLogger . hcServerCtx)
remoteSchemaPermsCtx <- asks (scRemoteSchemaPermsCtx . hcServerCtx)
functionPermsCtx <- asks (scFunctionPermsCtx . hcServerCtx)
let serverConfigCtx = ServerConfigCtx functionPermsCtx remoteSchemaPermsCtx sqlGenCtx
r <- withSCUpdate scRef logger $
runMetadataQuery env instanceId userInfo httpMgr sqlGenCtx remoteSchemaPermsCtx schemaCache query
runMetadataQuery env instanceId userInfo httpMgr serverConfigCtx schemaCache query
pure $ HttpResponse r []
v2QueryHandler
@ -453,7 +457,8 @@ v2QueryHandler query = do
instanceId <- asks (scInstanceId . hcServerCtx)
env <- asks (scEnvironment . hcServerCtx)
remoteSchemaPermsCtx <- asks (scRemoteSchemaPermsCtx . hcServerCtx)
V2Q.runQuery env instanceId userInfo schemaCache httpMgr sqlGenCtx remoteSchemaPermsCtx query
functionPermsCtx <- asks (scFunctionPermsCtx . hcServerCtx)
V2Q.runQuery env instanceId userInfo schemaCache httpMgr sqlGenCtx remoteSchemaPermsCtx functionPermsCtx query
v1Alpha1GQHandler
:: ( HasVersion
@ -727,13 +732,14 @@ mkWaiApp
-> RebuildableSchemaCache
-> EKG.Store
-> RemoteSchemaPermsCtx
-> FunctionPermissionsCtx
-> WS.ConnectionOptions
-> KeepAliveDelay
-- ^ Metadata storage connection pool
-> m HasuraApp
mkWaiApp env logger sqlGenCtx enableAL httpManager mode corsCfg enableConsole consoleAssetsDir
enableTelemetry instanceId apis lqOpts _ {- planCacheOptions -} responseErrorsConfig
liveQueryHook schemaCache ekgStore enableRSPermsCtx connectionOptions keepAliveDelay = do
liveQueryHook schemaCache ekgStore enableRSPermsCtx functionPermsCtx connectionOptions keepAliveDelay = do
-- See Note [Temporarily disabling query plan caching]
-- (planCache, schemaCacheRef) <- initialiseCache
@ -762,6 +768,7 @@ mkWaiApp env logger sqlGenCtx enableAL httpManager mode corsCfg enableConsole co
, scEnvironment = env
, scResponseInternalErrorsConfig = responseErrorsConfig
, scRemoteSchemaPermsCtx = enableRSPermsCtx
, scFunctionPermsCtx = functionPermsCtx
}
spockApp <- liftWithStateless $ \lowerIO ->

View File

@ -180,8 +180,7 @@ mkServeOptions rso = do
logHeadersFromEnv <- withEnvBool (rsoLogHeadersFromEnv rso) (fst logHeadersFromEnvEnv)
enableRemoteSchemaPerms <-
bool RemoteSchemaPermsDisabled RemoteSchemaPermsEnabled <$>
(withEnvBool (rsoEnableRemoteSchemaPermissions rso) $
(fst enableRemoteSchemaPermsEnv))
(withEnvBool (rsoEnableRemoteSchemaPermissions rso) (fst enableRemoteSchemaPermsEnv))
webSocketCompressionFromEnv <- withEnvBool (rsoWebSocketCompression rso) $
fst webSocketCompressionEnv
@ -195,12 +194,17 @@ mkServeOptions rso = do
webSocketKeepAlive <- KeepAliveDelay . fromIntegral . fromMaybe 5
<$> withEnv (rsoWebSocketKeepAlive rso) (fst webSocketKeepAliveEnv)
inferFunctionPerms <-
maybe FunctionPermissionsInferred (bool FunctionPermissionsManual FunctionPermissionsInferred) <$>
(withEnv (rsoInferFunctionPermissions rso) (fst inferFunctionPermsEnv))
return $ ServeOptions port host connParams txIso adminScrt authHook jwtSecret
unAuthRole corsCfg enableConsole consoleAssetsDir
enableTelemetry strfyNum enabledAPIs lqOpts enableAL
enabledLogs serverLogLevel planCacheOptions
internalErrorsConfig eventsHttpPoolSize eventsFetchInterval
logHeadersFromEnv enableRemoteSchemaPerms connectionOptions webSocketKeepAlive
inferFunctionPerms
where
#ifdef DeveloperAPIs
defaultAPIs = [METADATA,GRAPHQL,PGDUMP,CONFIG,DEVELOPER]
@ -544,6 +548,12 @@ enableRemoteSchemaPermsEnv =
, "Enables remote schema permissions (default: false)"
)
inferFunctionPermsEnv :: (String, String)
inferFunctionPermsEnv =
( "HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS"
, "Infers function permissions (default: true)"
)
adminInternalErrorsEnv :: (String, String)
adminInternalErrorsEnv =
@ -871,6 +881,13 @@ parseEnableRemoteSchemaPerms =
help (snd enableRemoteSchemaPermsEnv)
)
parseInferFunctionPerms :: Parser (Maybe Bool)
parseInferFunctionPerms = optional $
option ( eitherReader parseStrAsBool )
( long "infer-function-permissions" <>
help (snd inferFunctionPermsEnv)
)
mxRefetchDelayEnv :: (String, String)
mxRefetchDelayEnv =
( "HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL"
@ -983,6 +1000,7 @@ serveOptsToLog so =
, "remote_schema_permissions" J..= soEnableRemoteSchemaPermissions so
, "websocket_compression_options" J..= show (WS.connectionCompressionOptions . soConnectionOptions $ so)
, "websocket_keep_alive" J..= show (soWebsocketKeepAlive so)
, "infer_function_permissions" J..= soInferFunctionPermissions so
]
mkGenericStrLog :: L.LogLevel -> Text -> String -> StartupLog
@ -1031,6 +1049,7 @@ serveOptionsParser =
<*> parseEnableRemoteSchemaPerms
<*> parseWebSocketCompression
<*> parseWebSocketKeepAlive
<*> parseInferFunctionPerms
-- | This implements the mapping between application versions
-- and catalog schema versions.

View File

@ -74,6 +74,7 @@ data RawServeOptions impl
, rsoEnableRemoteSchemaPermissions :: !Bool
, rsoWebSocketCompression :: !Bool
, rsoWebSocketKeepAlive :: !(Maybe Int)
, rsoInferFunctionPermissions :: !(Maybe Bool)
}
-- | @'ResponseInternalErrorsConfig' represents the encoding of the internal
@ -124,6 +125,7 @@ data ServeOptions impl
, soEnableRemoteSchemaPermissions :: !RemoteSchemaPermsCtx
, soConnectionOptions :: !WS.ConnectionOptions
, soWebsocketKeepAlive :: !KeepAliveDelay
, soInferFunctionPermissions :: !FunctionPermissionsCtx
}
data DowngradeOptions

View File

@ -82,7 +82,7 @@ data SchemaSyncEvent
instance ToJSON SchemaSyncEvent where
toJSON = \case
SSEListenStart time -> String $ "event listening started at " <> tshow time
SSEPayload payload -> toJSON payload
SSEPayload payload -> toJSON payload
data ThreadError
= TEPayloadParse !Text
@ -192,12 +192,14 @@ startSchemaSyncProcessorThread
-> InstanceId
-> UTC.UTCTime
-> RemoteSchemaPermsCtx
-> FunctionPermissionsCtx
-> ManagedT m Immortal.Thread
startSchemaSyncProcessorThread sqlGenCtx logger httpMgr
schemaSyncEventRef cacheRef instanceId cacheInitStartTime remoteSchemaPermsCtx = do
schemaSyncEventRef cacheRef instanceId cacheInitStartTime remoteSchemaPermsCtx functionPermsCtx = do
-- Start processor thread
processorThread <- C.forkManagedT "SchemeUpdate.processor" logger $
processor sqlGenCtx logger httpMgr schemaSyncEventRef cacheRef instanceId cacheInitStartTime remoteSchemaPermsCtx
processor sqlGenCtx logger httpMgr schemaSyncEventRef
cacheRef instanceId cacheInitStartTime remoteSchemaPermsCtx functionPermsCtx
logThreadStarted logger instanceId TTProcessor processorThread
pure processorThread
@ -258,9 +260,10 @@ processor
-> InstanceId
-> UTC.UTCTime
-> RemoteSchemaPermsCtx
-> FunctionPermissionsCtx
-> m void
processor sqlGenCtx logger httpMgr updateEventRef
cacheRef instanceId cacheInitStartTime remoteSchemaPermsCtx =
cacheRef instanceId cacheInitStartTime remoteSchemaPermsCtx functionPermsCtx =
-- Never exits
forever $ do
event <- liftIO $ STM.atomically getLatestEvent
@ -281,7 +284,7 @@ processor sqlGenCtx logger httpMgr updateEventRef
when shouldReload $
refreshSchemaCache sqlGenCtx logger httpMgr cacheRef cacheInvalidations
threadType remoteSchemaPermsCtx "schema cache reloaded"
threadType remoteSchemaPermsCtx functionPermsCtx "schema cache reloaded"
where
-- checks if there is an event
-- and replaces it with Nothing
@ -307,10 +310,11 @@ refreshSchemaCache
-> CacheInvalidations
-> ThreadType
-> RemoteSchemaPermsCtx
-> FunctionPermissionsCtx
-> Text
-> m ()
refreshSchemaCache sqlGenCtx logger httpManager
cacheRef invalidations threadType remoteSchemaPermsCtx msg = do
cacheRef invalidations threadType remoteSchemaPermsCtx functionPermsCtx msg = do
-- Reload schema cache from catalog
eitherMetadata <- runMetadataStorageT fetchMetadata
resE <- runExceptT $ do
@ -325,7 +329,8 @@ refreshSchemaCache sqlGenCtx logger httpManager
Left e -> logError logger threadType $ TEQueryError e
Right () -> logInfo logger threadType $ object ["message" .= msg]
where
runCtx = RunCtx adminUserInfo httpManager sqlGenCtx remoteSchemaPermsCtx
serverConfigCtx = ServerConfigCtx functionPermsCtx remoteSchemaPermsCtx sqlGenCtx
runCtx = RunCtx adminUserInfo httpManager serverConfigCtx
logInfo :: (MonadIO m) => Logger Hasura -> ThreadType -> Value -> m ()
logInfo logger threadType val = unLogger logger $

View File

@ -164,7 +164,7 @@ computeMetrics sc _mtServiceTimings _mtPgVersion =
_mtEventTriggers = Map.size $ Map.filter (not . Map.null)
$ Map.map _tiEventTriggerInfoMap userTables
_mtRemoteSchemas = Map.size $ scRemoteSchemas sc
_mtFunctions = Map.size $ Map.filter (not . isSystemDefined . fiSystemDefined) pgFunctionCache
_mtFunctions = Map.size $ Map.filter (not . isSystemDefined . _fiSystemDefined) pgFunctionCache
_mtActions = computeActionsMetrics $ scActions sc
in Metrics{..}

View File

@ -37,7 +37,7 @@ newtype CacheRefT m a
= CacheRefT { runCacheRefT :: MVar RebuildableSchemaCache -> m a }
deriving
( Functor, Applicative, Monad, MonadIO, MonadError e, MonadBase b, MonadBaseControl b
, MonadTx, MonadUnique, UserInfoM, HTTP.HasHttpManagerM, HasSQLGenCtx)
, MonadTx, MonadUnique, UserInfoM, HTTP.HasHttpManagerM, HasServerConfigCtx)
via (ReaderT (MVar RebuildableSchemaCache) m)
instance MonadTrans CacheRefT where
@ -51,7 +51,7 @@ instance (MonadBase IO m) => CacheRM (CacheRefT m) where
askSchemaCache = CacheRefT (fmap lastBuiltSchemaCache . readMVar)
instance (MonadIO m, MonadBaseControl IO m, MonadTx m, HTTP.HasHttpManagerM m
, HasSQLGenCtx m, HasRemoteSchemaPermsCtx m, MonadResolveSource m) => CacheRWM (CacheRefT m) where
, MonadResolveSource m, HasServerConfigCtx m) => CacheRWM (CacheRefT m) where
buildSchemaCacheWithOptions reason invalidations metadata = CacheRefT $ flip modifyMVar \schemaCache -> do
((), cache, _) <- runCacheRWT schemaCache (buildSchemaCacheWithOptions reason invalidations metadata)
pure (cache, ())
@ -72,8 +72,7 @@ spec
, MonadBaseControl IO m
, MonadError QErr m
, HTTP.HasHttpManagerM m
, HasSQLGenCtx m
, HasRemoteSchemaPermsCtx m
, HasServerConfigCtx m
, MonadResolveSource m
)
=> SourceConfiguration -> PGExecCtx -> Q.ConnInfo -> SpecWithCache m

View File

@ -97,8 +97,8 @@ buildPostgresSpecs maybeUrlTemplate = do
setupCacheRef = do
httpManager <- HTTP.newManager HTTP.tlsManagerSettings
let sqlGenCtx = SQLGenCtx False
cacheBuildParams = CacheBuildParams httpManager sqlGenCtx RemoteSchemaPermsDisabled
(mkPgSourceResolver print)
serverConfigCtx = ServerConfigCtx FunctionPermissionsInferred RemoteSchemaPermsDisabled sqlGenCtx
cacheBuildParams = CacheBuildParams httpManager (mkPgSourceResolver print) serverConfigCtx
run :: CacheBuild a -> IO a
run =

View File

@ -82,6 +82,13 @@ def pytest_addoption(parser):
help="Run testcases for logging"
)
parser.addoption(
"--test-function-permissions",
action="store_true",
required=False,
help="Run manual function permission tests"
)
parser.addoption(
"--test-jwk-url",
action="store_true",
@ -283,6 +290,12 @@ def actions_fixture(hge_ctx):
webhook_httpd.server_close()
web_server.join()
@pytest.fixture(scope='class')
def functions_permissions_fixtures(hge_ctx):
if not hge_ctx.function_permissions:
pytest.skip('These tests are meant to be run with --test-function-permissions set')
return
@pytest.fixture(scope='class')
def scheduled_triggers_evts_webhook(request):
webhook_httpd = EvtsWebhookServer(server_address=('127.0.0.1', 5594))

View File

@ -473,6 +473,7 @@ class HGECtx:
self.webhook_insecure = config.getoption('--test-webhook-insecure')
self.metadata_disabled = config.getoption('--test-metadata-disabled')
self.may_skip_test_teardown = False
self.function_permissions = config.getoption('--test-function-permissions')
self.engine = create_engine(self.pg_url)
self.meta = MetaData()

View File

@ -0,0 +1,4 @@
type: pg_create_function_permission
args:
function: add_to_score
role: anonymous

View File

@ -0,0 +1,4 @@
type: pg_drop_function_permission
args:
function: add_to_score
role: anonymous

View File

@ -0,0 +1,20 @@
- description: Fails as anonymous role doesn't have access to the `add_to_score` function
headers:
X-Hasura-Role: anonymous
url: /v1/graphql
status: 200
query:
query: |
mutation {
add_to_score(args: {search: "Black"}){
name
score
role_echo
}
}
response:
errors:
- extensions:
path: $
code: validation-failed
message: no mutations exist

View File

@ -0,0 +1,4 @@
type: pg_create_function_permission
args:
role: user
function: get_articles

View File

@ -0,0 +1,23 @@
- description: Query function as a role without permission being configured for the role
url: /v1/graphql
status: 200
headers:
X-Hasura-Role: user
X-Hasura-User-Id: '1'
response:
data:
get_articles:
- title: Article 1
content: Sample article content 1
- title: Article 2
content: Sample article content 2
- title: Article 3
content: Sample article content 3
query:
query: |
query {
get_articles(args: {search: "art"}) {
title
content
}
}

View File

@ -0,0 +1,19 @@
description: Query function as a role without permission being configured for the role
url: /v1/graphql
status: 200
headers:
X-Hasura-Role: user
response:
errors:
- extensions:
path: $.selectionSet.get_articles
code: validation-failed
message: "field \"get_articles\" not found in type: 'query_root'"
query:
query: |
query {
get_articles(args: {search: "art"}) {
title
content
}
}

View File

@ -0,0 +1,71 @@
type: bulk
args:
- type: run_sql
args:
sql: |
create table author(
id serial primary key,
name text unique,
is_registered boolean not null default false,
remarks_internal text
);
INSERT INTO author (name, remarks_internal)
VALUES
('Author 1', 'remark 1'),
('Author 2', 'remark 2'),
('Author 3', 'remark 3');
CREATE TABLE article (
id SERIAL PRIMARY KEY,
title TEXT,
content TEXT,
author_id INTEGER NOT NULL REFERENCES author(id),
is_published BOOLEAN NOT NULL default FALSE,
published_on TIMESTAMP
);
INSERT INTO article (title, content, author_id, is_published)
VALUES
('Article 1', 'Sample article content 1', 1, false),
('Article 2', 'Sample article content 2', 1, true),
('Article 3', 'Sample article content 3', 2, true),
('Article 4', 'Sample article content 4', 3, false);
CREATE FUNCTION get_articles(search text)
RETURNS SETOF article AS $$
SELECT *
FROM article
WHERE
title ilike ('%' || search || '%')
OR content ilike ('%' || search || '%')
$$ LANGUAGE sql STABLE;
- type: track_table
args:
table: author
- type: track_table
args:
table: article
- type: track_function
args:
name: get_articles
schema: public
- type: create_select_permission
args:
table: article
role: user
permission:
columns:
- title
- content
- is_published
filter:
_or:
- id: X-HASURA-USER-ID
- is_published:
_eq: true

View File

@ -0,0 +1,9 @@
type: bulk
args:
- type: run_sql
args:
sql: |
DROP TABLE article cascade;
DROP TABLE author cascade;
cascade: true

View File

@ -588,9 +588,14 @@ class TestGraphQLMutateEnums:
def test_delete_where_enum_field(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/delete_where_enum_field.yaml', transport)
use_function_permission_fixtures = usefixtures(
'per_class_db_schema_for_mutation_tests',
'per_method_db_data_for_mutation_tests',
'functions_permissions_fixtures'
)
# Tracking VOLATILE SQL functions as mutations, or queries (#1514)
@pytest.mark.parametrize('transport', ['http', 'websocket'])
@use_mutation_fixtures
@use_function_permission_fixtures
class TestGraphQLMutationFunctions:
@classmethod
def dir(cls):
@ -609,7 +614,17 @@ class TestGraphQLMutationFunctions:
def test_functions_as_mutations(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/function_as_mutations.yaml', transport)
# When graphql-engine is started with `--infer-function-permissions=false` then
# a function is only accessible to a role when the permission is granted through
# the `pg_create_function_permission` definition
def test_function_as_mutation_without_function_permission(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/function_without_function_permission.yaml')
# Ensure select permissions on the corresponding SETOF table apply to
# the return set of the mutation field backed by the tracked function.
def test_functions_as_mutations_permissions(self, hge_ctx, transport):
st_code, resp = hge_ctx.v1metadataq_f(self.dir() + '/create_function_permission_add_to_score.yaml')
assert st_code == 200, resp
check_query_f(hge_ctx, self.dir() + '/function_as_mutations_permissions.yaml', transport)
st_code, resp = hge_ctx.v1metadataq_f(self.dir() + '/drop_function_permission_add_to_score.yaml')
assert st_code == 200, resp

View File

@ -806,3 +806,24 @@ def _test_relay_pagination(hge_ctx, transport, test_file_prefix, no_of_pages):
page_no = i + 1
test_file = "page_" + str(page_no) + ".yaml"
check_query_f(hge_ctx, test_file_prefix + "/" + test_file, transport)
use_function_permission_fixtures = pytest.mark.usefixtures(
'per_method_tests_db_state',
'functions_permissions_fixtures'
)
@pytest.mark.parametrize('transport', ['http', 'websocket'])
@use_function_permission_fixtures
class TestGraphQLQueryFunctionPermissions:
@classmethod
def dir(cls):
return 'queries/graphql_query/functions/permissions/'
def test_access_function_without_permission_configured(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + 'get_articles_without_permission_configured.yaml')
def test_access_function_with_permission_configured(self, hge_ctx, transport):
st_code, resp = hge_ctx.v1metadataq_f(self.dir() + 'add_function_permission_get_articles.yaml')
assert st_code == 200, resp
check_query_f(hge_ctx, self.dir() + 'get_articles_with_permission_configured.yaml')