mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
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:
parent
0767333597
commit
10a3f9960d
@ -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"
|
||||
|
10
CHANGELOG.md
10
CHANGELOG.md
@ -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,7 +77,7 @@ 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
|
||||
|
@ -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 |
|
||||
+-----------------+-----------------------------------------+------------------+
|
||||
+----------------------------------+---------------------------------------------------+------------------+
|
||||
| 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>
|
||||
|
62
docs/graphql/core/api-reference/metadata-api/dataerrors.csv
Normal file
62
docs/graphql/core/api-reference/metadata-api/dataerrors.csv
Normal 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
|
|
164
docs/graphql/core/api-reference/metadata-api/index.rst
Normal file
164
docs/graphql/core/api-reference/metadata-api/index.rst
Normal 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.
|
154
docs/graphql/core/api-reference/schema-api/index.rst
Normal file
154
docs/graphql/core/api-reference/schema-api/index.rst
Normal 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.
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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,18 +448,21 @@ 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
|
||||
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) $ selectFunction function displayName (Just functionDesc) perms
|
||||
, mapMaybeFieldParser (asDbRootField . QDBAggregation) $ selectFunctionAggregate function aggName (Just aggDesc) perms
|
||||
[ 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
|
||||
@ -466,13 +470,16 @@ buildQueryFields sourceName sourceConfig tables (takeExposedAs FEAQuery id -> fu
|
||||
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,21 +769,31 @@ 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
|
||||
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
|
||||
"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
|
||||
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
|
||||
]
|
||||
@ -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")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
@ -178,6 +205,7 @@ purgeMetadataObj = \case
|
||||
MTOComputedField ccn -> dropComputedFieldInMetadata ccn
|
||||
MTORemoteRelationship rn -> dropRemoteRelationshipInMetadata rn
|
||||
SMOFunction qf -> dropFunctionInMetadata source qf
|
||||
SMOFunctionPermission qf rn -> dropFunctionPermissionInMetadata source qf rn
|
||||
MORemoteSchema rsn -> dropRemoteSchemaInMetadata rsn
|
||||
MORemoteSchemaPermissions rsName role -> dropRemoteSchemaPermissionInMetadata rsName role
|
||||
MOCustomTypes -> clearCustomTypesInMetadata
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 "
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -118,9 +118,8 @@ $(makeLenses ''BuildOutputs)
|
||||
data CacheBuildParams
|
||||
= CacheBuildParams
|
||||
{ _cbpManager :: !HTTP.Manager
|
||||
, _cbpSqlGenCtx :: !SQLGenCtx
|
||||
, _cbpRemoteSchemaPermsCtx :: !RemoteSchemaPermsCtx
|
||||
, _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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 []
|
||||
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) =
|
||||
|
@ -26,8 +26,7 @@ data RunCtx
|
||||
= RunCtx
|
||||
{ _rcUserInfo :: !UserInfo
|
||||
, _rcHttpMgr :: !HTTP.Manager
|
||||
, _rcSqlGenCtx :: !SQLGenCtx
|
||||
, _rcRemoteSchemaPermsCtx :: !RemoteSchemaPermsCtx
|
||||
, _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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
@ -103,7 +105,7 @@ runQueryM
|
||||
, MonadBaseControl IO m
|
||||
, UserInfoM m
|
||||
, CacheRWM m
|
||||
, HasSQLGenCtx m
|
||||
, HasServerConfigCtx m
|
||||
, Tracing.MonadTrace m
|
||||
, MetadataM m
|
||||
)
|
||||
|
@ -113,6 +113,7 @@ data ServerCtx
|
||||
, scResponseInternalErrorsConfig :: !ResponseInternalErrorsConfig
|
||||
, scEnvironment :: !Env.Environment
|
||||
, scRemoteSchemaPermsCtx :: !RemoteSchemaPermsCtx
|
||||
, scFunctionPermsCtx :: !FunctionPermissionsCtx
|
||||
}
|
||||
|
||||
data HandlerCtx
|
||||
@ -399,7 +400,8 @@ v1QueryHandler query = do
|
||||
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
|
||||
@ -423,8 +425,10 @@ v1MetadataHandler query = do
|
||||
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 ->
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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 $
|
||||
|
@ -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{..}
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
|
@ -0,0 +1,4 @@
|
||||
type: pg_create_function_permission
|
||||
args:
|
||||
function: add_to_score
|
||||
role: anonymous
|
@ -0,0 +1,4 @@
|
||||
type: pg_drop_function_permission
|
||||
args:
|
||||
function: add_to_score
|
||||
role: anonymous
|
@ -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
|
@ -0,0 +1,4 @@
|
||||
type: pg_create_function_permission
|
||||
args:
|
||||
role: user
|
||||
function: get_articles
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
@ -0,0 +1,9 @@
|
||||
type: bulk
|
||||
args:
|
||||
|
||||
- type: run_sql
|
||||
args:
|
||||
sql: |
|
||||
DROP TABLE article cascade;
|
||||
DROP TABLE author cascade;
|
||||
cascade: true
|
@ -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
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user