diff --git a/.circleci/test-server.sh b/.circleci/test-server.sh index 216b50bc939..1ed9f257368 100755 --- a/.circleci/test-server.sh +++ b/.circleci/test-server.sh @@ -6,7 +6,7 @@ echo "Running tests on node $CIRCLE_NODE_INDEX of $CIRCLE_NODE_TOTAL" if [ -z "$SERVER_TEST_TO_RUN" ]; then echo 'Please specify $SERVER_TEST_TO_RUN' exit 1 -else +else echo "Running test $SERVER_TEST_TO_RUN" fi @@ -522,7 +522,7 @@ case "$SERVER_TEST_TO_RUN" in unset HASURA_GRAPHQL_CORS_DOMAIN ;; - + ws-init-cookie-read-cors-enabled) # test websocket transport with initial cookie header @@ -660,6 +660,22 @@ case "$SERVER_TEST_TO_RUN" in kill_hge_servers ;; + function-permissions) + echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH FUNCTION PERMISSIONS ENABLED ########>\n" + TEST_TYPE="remote-schema-permissions" + export HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS=false + + run_hge_with_args serve + wait_for_port 8080 + + pytest -n 1 -vv --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --test_function_permissions test_graphql_queries.py::TestGraphQLQueryFunctionPermissions + pytest -n 1 -vv --hge-urls "$HGE_URL" --pg-urls "$HASURA_GRAPHQL_DATABASE_URL" --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --test_function_permissions test_graphql_mutations.py::TestGraphQLMutationFunctions + + unset HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS + + kill_hge_servers + ;; + query-caching) echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE QUERY CACHING #####################################>\n" TEST_TYPE="query-caching" @@ -705,18 +721,18 @@ case "$SERVER_TEST_TO_RUN" in if [ "$RUN_WEBHOOK_TESTS" == "true" ] ; then TEST_TYPE="post-webhook" echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH ADMIN SECRET & WEBHOOK (POST) #########################>\n" - + export HASURA_GRAPHQL_AUTH_HOOK="https://localhost:9090/" export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM" init_ssl - + start_multiple_hge_servers - + python3 webhook.py 9090 "$OUTPUT_FOLDER/ssl/webhook-key.pem" "$OUTPUT_FOLDER/ssl/webhook.pem" > "$OUTPUT_FOLDER/webhook.log" 2>&1 & WH_PID=$! wait_for_port 9090 - + run_pytest_parallel --hge-key="$HASURA_GRAPHQL_ADMIN_SECRET" --hge-webhook="$HASURA_GRAPHQL_AUTH_HOOK" - + kill_hge_servers fi ;; diff --git a/CHANGELOG.md b/CHANGELOG.md index ee1ae15cb62..b0457a7a01f 100644 --- a/CHANGELOG.md +++ b/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,12 +77,12 @@ variable ``HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS`` set to ``true``. - if a query selects table `bar` through table `foo` via a relationship, the required permissions headers will be the union of the required headers of table `foo` and table `bar` (we used to only check the headers of the root table); - if an insert does not have an `on_conflict` clause, it will not require the update permissions headers. -This release contains the remote schema permissions feature, which introduces a breaking change: +- This release contains the remote schema permissions feature, which introduces a breaking change: -Earlier, remote schemas were considered to be a public entity and all the roles had unrestricted -access to the remote schema. If remote schema permissions are enabled in the graphql-engine, a given -remote schema will only be accessible to a role ,if the role has permissions configured for the said remote schema -and be accessible according to the permissions that were configured for the role. + Earlier, remote schemas were considered to be a public entity and all the roles had unrestricted + access to the remote schema. If remote schema permissions are enabled in the graphql-engine, a given + remote schema will only be accessible to a role ,if the role has permissions configured for the said remote schema + and be accessible according to the permissions that were configured for the role. ### Bug fixes and improvements diff --git a/docs/graphql/core/api-reference/index.rst b/docs/graphql/core/api-reference/index.rst index e6a249aab18..4ae4f011739 100644 --- a/docs/graphql/core/api-reference/index.rst +++ b/docs/graphql/core/api-reference/index.rst @@ -15,29 +15,33 @@ API Reference Available APIs -------------- -+-----------------+-----------------------------------------+------------------+ -| API | Endpoint | Access | -+=================+=========================================+==================+ -| GraphQL | :ref:`/v1/graphql ` | Permission rules | -+-----------------+-----------------------------------------+------------------+ -| Relay | :ref:`/v1beta1/relay ` | Permission rules | -+-----------------+-----------------------------------------+------------------+ -| Legacy GraphQL | :ref:`/v1alpha1/graphql ` | Permission rules | -+-----------------+-----------------------------------------+------------------+ -| Schema/Metadata | :ref:`/v1/query ` | Admin only | -+-----------------+-----------------------------------------+------------------+ -| RESTified GQL | :ref:`/api/rest ` | GQL REST Routes | -+-----------------+-----------------------------------------+------------------+ -| Version | :ref:`/v1/version ` | Public | -+-----------------+-----------------------------------------+------------------+ -| Health | :ref:`/healthz ` | Public | -+-----------------+-----------------------------------------+------------------+ -| PG Dump | :ref:`/v1alpha1/pg_dump ` | Admin only | -+-----------------+-----------------------------------------+------------------+ -| Config | :ref:`/v1alpha1/config ` | Admin only | -+-----------------+-----------------------------------------+------------------+ -| Explain | :ref:`/v1/graphql/explain `| Admin only | -+-----------------+-----------------------------------------+------------------+ ++----------------------------------+---------------------------------------------------+------------------+ +| API | Endpoint | Access | ++==================================+===================================================+==================+ +| GraphQL | :ref:`/v1/graphql ` | Permission rules | ++----------------------------------+---------------------------------------------------+------------------+ +| Relay | :ref:`/v1beta1/relay ` | Permission rules | ++----------------------------------+---------------------------------------------------+------------------+ +| Legacy GraphQL | :ref:`/v1alpha1/graphql ` | Permission rules | ++----------------------------------+---------------------------------------------------+------------------+ +| Schema/Metadata *(< v1.3)* | :ref:`/v1/query ` | Admin only | ++----------------------------------+---------------------------------------------------+------------------+ +| Schema *(> v1.4)* | :ref:`/v2/query ` | Admin only | ++----------------------------------+---------------------------------------------------+------------------+ +| Metadata *(> v1.4)* | :ref:`/v1/metadata ` | Admin only | ++----------------------------------+---------------------------------------------------+------------------+ +| Restified GQL | :ref:`/api/rest ` | GQL REST Routes | ++----------------------------------+---------------------------------------------------+------------------+ +| Version | :ref:`/v1/version ` | Public | ++----------------------------------+---------------------------------------------------+------------------+ +| Health | :ref:`/healthz ` | Public | ++----------------------------------+---------------------------------------------------+------------------+ +| PG Dump | :ref:`/v1alpha1/pg_dump ` | Admin only | ++----------------------------------+---------------------------------------------------+------------------+ +| Config | :ref:`/v1alpha1/config ` | Admin only | ++----------------------------------+---------------------------------------------------+------------------+ +| Explain | :ref:`/v1/graphql/explain ` | 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 Relay GraphQL API - Schema / Metadata APIs + Schema / Metadata APIs V1 + Schema APIs + Metadata APIs RESTified GraphQL Endpoints Version API Health check API diff --git a/docs/graphql/core/api-reference/metadata-api/dataerrors.csv b/docs/graphql/core/api-reference/metadata-api/dataerrors.csv new file mode 100644 index 00000000000..3b8540ab7cc --- /dev/null +++ b/docs/graphql/core/api-reference/metadata-api/dataerrors.csv @@ -0,0 +1,62 @@ +Status Code,Code,Error +400,postgres-error,Not-NULL violation. null value in column violates not-null constraint +400,permission-denied,select on for role is not allowed. +400,not-exists,table does not exist +400,not-exists,no such table/view exists in postgres : +400,not-exists, does not exist +400,already-tracked,view/table already tracked : +400,access-denied,restricted access : admin only +400,not-supported,table renames are not yet supported : +400,not-exists, does not exist +400,already-exists,cannot add column in table as a relationship with the name already exists +400,invalid-json,invalid json +400,not-supported,column renames are not yet supported : . +400,invalid-headers,missing header : +400,dependency-error,cannot change type of column in table because of the following dependencies : +400,invalid-headers,X-Hasura-User-Id should be an integer +400,dependency-error,cannot drop due to the following dependent objects : +400,access-denied,You have to be admin to access this endpoint +400,parse-failed,parsing dotted table failed : +400,access-denied,not authorised to access this tx +400,already-exists,multiple declarations exist for the following : +400,not-exists,tx does not exists +400,already-exists,column/relationship of table already exists +400,already-initialised,the state seems to be initialised already. \ \ you may need to migrate from this version: +400,constraint-error,no foreign constraint exists on the given column +400,not-supported,unsupported version : +400,constraint-error,more than one foreign key constraint exists on the given column +400,already-exists,the query template already exists +400,permission-error,' permission on for role already exists +400,permission-error,' permission on for role does not exist +400,unexpected-payload,Unknown operator : +400,unexpected-payload,expecting a string for column operator +400,unexpected-payload,"incompatible column types : '', '' " +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 '' +400,unexpected-payload,objects should not be empty +400,invalid-params,missing parameter : +400,unexpected-payload,can't be empty +400,,' is a relationship and should be expanded +400,unexpected-payload,' should be included in 'columns' +400,unexpected-payload,' is an array relationship and can't be used in 'order_by' +400,,' 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, on for role is not allowed +400,not-exists,no such column exists : +400,permission-denied,role does not have permission to column +400,,"expecting a postgres column; but, is relationship" +400,unexpected-payload,JSON column can not be part of where clause +400,unexpected-payload,is of type ; this operator works only on column of types <[types]> +400,postgres-error,query execution failed +500,unexpected,unexpected dependency of relationship : +500,unexpected,unexpected dependent object : +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 diff --git a/docs/graphql/core/api-reference/metadata-api/index.rst b/docs/graphql/core/api-reference/metadata-api/index.rst new file mode 100644 index 00000000000..1b7cd45c0ae --- /dev/null +++ b/docs/graphql/core/api-reference/metadata-api/index.rst @@ -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": "", + "args": + } + +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 ` array + - 1 + - Execute multiple operations in a single query + + * - :ref:`pg_create_function_permission` + - :ref:`pg_create_function_permission_args ` + - 1 + - Create a function permission + + * - :ref:`pg_drop_function_permission` + - :ref:`pg_drop_function_permission_args ` + - 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. \ No newline at end of file diff --git a/docs/graphql/core/api-reference/schema-api/index.rst b/docs/graphql/core/api-reference/schema-api/index.rst new file mode 100644 index 00000000000..8fc014fc4bf --- /dev/null +++ b/docs/graphql/core/api-reference/schema-api/index.rst @@ -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": "", + "args": + } + +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 ` array + - 1 + - Execute multiple operations in a single query + + * - :ref:`run_sql` + - :ref:`run_sql_args ` + - 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. \ No newline at end of file diff --git a/docs/graphql/core/api-reference/schema-metadata-api/custom-functions.rst b/docs/graphql/core/api-reference/schema-metadata-api/custom-functions.rst index 42ca6b7d0f1..797ca1bdc9c 100644 --- a/docs/graphql/core/api-reference/schema-metadata-api/custom-functions.rst +++ b/docs/graphql/core/api-reference/schema-metadata-api/custom-functions.rst @@ -107,7 +107,7 @@ In most cases you will want ``VOLATILE`` functions to only be exposed as mutations, and only ``STABLE`` and ``IMMUTABLE`` functions to be queries. When tracking ``VOLATILE`` functions under the ``query`` root, the user needs to guarantee that the field is idempotent and side-effect free, in the context -of the resulting GraphQL API. +of the resulting GraphQL API. One such use case might be a function that wraps a simple query and performs some logging visible only to administrators. @@ -170,6 +170,102 @@ Function Configuration - **Return type**: MUST be ``SETOF `` where ```` 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 ` + - Name of the SQL function + * - role + - true + - :ref:`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 ` + - Name of the SQL function + * - role + - true + - :ref:`RoleName ` + - Name of the role + * - source + - false + - Text + - Name of the source of the SQL function + + .. _untrack_function: untrack_function diff --git a/docs/graphql/core/api-reference/schema-metadata-api/index.rst b/docs/graphql/core/api-reference/schema-metadata-api/index.rst index f56894e9910..3925b99391a 100644 --- a/docs/graphql/core/api-reference/schema-metadata-api/index.rst +++ b/docs/graphql/core/api-reference/schema-metadata-api/index.rst @@ -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 diff --git a/docs/graphql/core/api-reference/schema-metadata-api/remote-schema-permissions.rst b/docs/graphql/core/api-reference/schema-metadata-api/remote-schema-permissions.rst index 177370d05d4..bb2a693c086 100644 --- a/docs/graphql/core/api-reference/schema-metadata-api/remote-schema-permissions.rst +++ b/docs/graphql/core/api-reference/schema-metadata-api/remote-schema-permissions.rst @@ -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 diff --git a/docs/graphql/core/deployment/graphql-engine-flags/reference.rst b/docs/graphql/core/deployment/graphql-engine-flags/reference.rst index bd037bf1707..75ac33f4ee0 100644 --- a/docs/graphql/core/deployment/graphql-engine-flags/reference.rst +++ b/docs/graphql/core/deployment/graphql-engine-flags/reference.rst @@ -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. diff --git a/docs/graphql/core/schema/custom-functions.rst b/docs/graphql/core/schema/custom-functions.rst index d7e315d336b..89e7c0881e5 100644 --- a/docs/graphql/core/schema/custom-functions.rst +++ b/docs/graphql/core/schema/custom-functions.rst @@ -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. diff --git a/server/src-exec/Main.hs b/server/src-exec/Main.hs index 7e2dad851fa..c05b2681f9c 100644 --- a/server/src-exec/Main.hs +++ b/server/src-exec/Main.hs @@ -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 diff --git a/server/src-lib/Hasura/App.hs b/server/src-lib/Hasura/App.hs index 22fb1a75178..68ed681a966 100644 --- a/server/src-lib/Hasura/App.hs +++ b/server/src-lib/Hasura/App.hs @@ -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 ) diff --git a/server/src-lib/Hasura/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs index b680cf5913a..888f4d42585 100644 --- a/server/src-lib/Hasura/GraphQL/Schema.hs +++ b/server/src-lib/Hasura/GraphQL/Schema.hs @@ -77,8 +77,7 @@ buildGQLContext , MonadError QErr m , MonadIO m , MonadUnique m - , HasSQLGenCtx m - , HasRemoteSchemaPermsCtx m + , HasServerConfigCtx m ) => ( GraphQLQueryType , SourceCache @@ -92,9 +91,8 @@ buildGQLContext ) buildGQLContext = proc (queryType, pgSources, allRemoteSchemas, allActions, nonObjectCustomTypes) -> do - - sqlGenCtx@(SQLGenCtx{ stringifyNum }) <- bindA -< askSQLGenCtx - remoteSchemaPermsCtx <- bindA -< askRemoteSchemaPermsCtx + ServerConfigCtx functionPermsCtx remoteSchemaPermsCtx sqlGenCtx@(SQLGenCtx stringifyNum) <- + bindA -< askServerConfigCtx let remoteSchemasRoles = concatMap (Map.keys . _rscPermissions . fst . snd) $ Map.toList allRemoteSchemas @@ -108,7 +106,9 @@ buildGQLContext = allRemoteSchemas <&> (\(remoteSchemaCtx, _metadataObj) -> (_rscIntro remoteSchemaCtx, _rscParsed remoteSchemaCtx)) - adminQueryContext = QueryContext stringifyNum queryType adminRemoteRelationshipQueryCtx + -- The function permissions context doesn't actually matter because the + -- admin will have access to the function anyway + adminQueryContext = QueryContext stringifyNum queryType adminRemoteRelationshipQueryCtx FunctionPermissionsInferred -- build the admin DB-only context so that we can check against name clashes with remotes -- TODO: Is there a better way to check for conflicts without actually building the admin schema? @@ -142,10 +142,10 @@ buildGQLContext = ( Set.toMap allRoles & Map.traverseWithKey \roleName () -> case queryType of QueryHasura -> - buildRoleContext (sqlGenCtx, queryType) pgSources allRemoteSchemas allActionInfos + buildRoleContext (sqlGenCtx, queryType, functionPermsCtx) pgSources allRemoteSchemas allActionInfos nonObjectCustomTypes remotes roleName remoteSchemaPermsCtx QueryRelay -> - buildRelayRoleContext (sqlGenCtx, queryType) pgSources allActionInfos + buildRelayRoleContext (sqlGenCtx, queryType, functionPermsCtx) pgSources allActionInfos nonObjectCustomTypes adminMutationRemotes roleName ) unauthenticated <- bindA -< unauthenticatedContext adminQueryRemotes adminMutationRemotes remoteSchemaPermsCtx @@ -187,13 +187,13 @@ buildRoleBasedRemoteSchemaParser role remoteSchemaCache = do -- TODO: Integrate relay schema buildRoleContext :: (MonadError QErr m, MonadIO m, MonadUnique m) - => (SQLGenCtx, GraphQLQueryType) -> SourceCache -> RemoteSchemaCache + => (SQLGenCtx, GraphQLQueryType, FunctionPermissionsCtx) -> SourceCache -> RemoteSchemaCache -> [ActionInfo 'Postgres] -> NonObjectTypeMap -> [( RemoteSchemaName , (IntrospectionResult, ParsedIntrospection))] -> RoleName -> RemoteSchemaPermsCtx -> m (RoleContext GQLContext) -buildRoleContext (SQLGenCtx stringifyNum, queryType) sources +buildRoleContext (SQLGenCtx stringifyNum, queryType, functionPermsCtx) sources allRemoteSchemas allActionInfos nonObjectCustomTypes remotes roleName remoteSchemaPermsCtx = do roleBasedRemoteSchemas <- @@ -206,7 +206,7 @@ buildRoleContext (SQLGenCtx stringifyNum, queryType) sources let queryRemotes = getQueryRemotes $ snd . snd <$> roleBasedRemoteSchemas mutationRemotes = getMutationRemotes $ snd . snd <$> roleBasedRemoteSchemas remoteRelationshipQueryContext = Map.fromList roleBasedRemoteSchemas - roleQueryContext = QueryContext stringifyNum queryType remoteRelationshipQueryContext + roleQueryContext = QueryContext stringifyNum queryType remoteRelationshipQueryContext functionPermsCtx runMonadSchema roleName roleQueryContext sources $ do let pgSources = mapMaybe unsafeSourceInfo $ toList sources @@ -252,7 +252,6 @@ buildRoleContext (SQLGenCtx stringifyNum, queryType) sources -> [P.FieldParser (P.ParseT Identity) RemoteField] getMutationRemotes = concatMap (concat . piMutation) - buildFullestDBSchema :: (MonadError QErr m, MonadIO m, MonadUnique m) => QueryContext -> SourceCache -> [ActionInfo 'Postgres] -> NonObjectTypeMap @@ -287,16 +286,16 @@ buildFullestDBSchema queryContext sources allActionInfos nonObjectCustomTypes = buildRelayRoleContext :: (MonadError QErr m, MonadIO m, MonadUnique m) - => (SQLGenCtx, GraphQLQueryType) -> SourceCache -> [ActionInfo 'Postgres] -> NonObjectTypeMap + => (SQLGenCtx, GraphQLQueryType, FunctionPermissionsCtx) -> SourceCache -> [ActionInfo 'Postgres] -> NonObjectTypeMap -> [P.FieldParser (P.ParseT Identity) RemoteField] -> RoleName -> m (RoleContext GQLContext) -buildRelayRoleContext (SQLGenCtx stringifyNum, queryType) sources +buildRelayRoleContext (SQLGenCtx stringifyNum, queryType, functionPermsCtx) sources allActionInfos nonObjectCustomTypes mutationRemotes roleName = -- TODO: At the time of writing this, remote schema queries are not supported in relay. -- When they are supported, we should get do what `buildRoleContext` does. Since, they -- are not supported yet, we use `mempty` below for `RemoteRelationshipQueryContext`. - let roleQueryContext = QueryContext stringifyNum queryType mempty + let roleQueryContext = QueryContext stringifyNum queryType mempty functionPermsCtx in runMonadSchema roleName roleQueryContext sources $ do @@ -432,6 +431,8 @@ buildQueryFields -> [FunctionInfo b] -> m [P.FieldParser n (QueryRootField (UnpreparedValue b))] buildQueryFields sourceName sourceConfig tables (takeExposedAs FEAQuery id -> functions) = do + functionPermsCtx <- asks $ qcFunctionPermsContext . getter + roleName <- askRoleName tableSelectExpParsers <- for tables \(table, _tableInfo) -> do selectPerms <- tableSelectPermissions table customRootFields <- _tcCustomRootFields . _tciCustomConfig . _tiCoreInfo <$> askTableInfo @'Postgres table @@ -447,32 +448,38 @@ buildQueryFields sourceName sourceConfig tables (takeExposedAs FEAQuery id -> fu , mapMaybeFieldParser (asDbRootField . QDBPrimaryKey) $ selectTableByPk table (fromMaybe pkName $ _tcrfSelectByPk customRootFields) (Just pkDesc) perms , mapMaybeFieldParser (asDbRootField . QDBAggregation) $ selectTableAggregate table (fromMaybe aggName $ _tcrfSelectAggregate customRootFields) (Just aggDesc) perms ] - functionSelectExpParsers <- for functions \function -> do - let targetTable = fiReturnType function - functionName = fiName function - selectPerms <- tableSelectPermissions targetTable - for selectPerms \perms -> do - displayName <- functionGraphQLName @b functionName `onLeft` throwError - let functionDesc = G.Description $ "execute function " <> functionName <<> " which returns " <>> targetTable - aggName = displayName <> $$(G.litName "_aggregate") - aggDesc = G.Description $ "execute function " <> functionName <<> " and query aggregates on result of table type " <>> targetTable - catMaybes <$> sequenceA - [ requiredFieldParser (asDbRootField . QDBSimple) $ selectFunction function displayName (Just functionDesc) perms - , mapMaybeFieldParser (asDbRootField . QDBAggregation) $ selectFunctionAggregate function aggName (Just aggDesc) perms - ] + functionSelectExpParsers <- for functions \function -> runMaybeT $ do + let targetTable = _fiReturnType function + functionName = _fiName function + selectPerms <- lift $ tableSelectPermissions targetTable + perms <- hoistMaybe selectPerms + when (functionPermsCtx == FunctionPermissionsManual) $ + -- see Note [Function Permissions] + guard $ roleName == adminRoleName || roleName `elem` (_fiPermissions function) + displayName <- functionGraphQLName @b functionName `onLeft` throwError + let functionDesc = G.Description $ "execute function " <> functionName <<> " which returns " <>> targetTable + aggName = displayName <> $$(G.litName "_aggregate") + aggDesc = G.Description $ "execute function " <> functionName <<> " and query aggregates on result of table type " <>> targetTable + catMaybes <$> sequenceA + [ requiredFieldParser (asDbRootField . QDBSimple) $ lift $ selectFunction function displayName (Just functionDesc) perms + , mapMaybeFieldParser (asDbRootField . QDBAggregation) $ lift $ selectFunctionAggregate function aggName (Just aggDesc) perms + ] pure $ (concat . catMaybes) (tableSelectExpParsers <> functionSelectExpParsers) where asDbRootField = let pgExecCtx = PG._pscExecCtx sourceConfig in RFDB sourceName pgExecCtx - mapMaybeFieldParser :: (a -> a') -> m (Maybe (P.FieldParser n a)) -> m (Maybe (P.FieldParser n a')) - mapMaybeFieldParser f = fmap $ fmap $ fmap f - requiredFieldParser :: (Functor n, Functor m)=> (a -> b) -> m (P.FieldParser n a) -> m (Maybe (P.FieldParser n b)) requiredFieldParser f = fmap $ Just . fmap f +mapMaybeFieldParser + :: (Functor n, Functor m) + => (a -> b) + -> m (Maybe (P.FieldParser n a)) + -> m (Maybe (P.FieldParser n b)) +mapMaybeFieldParser f = fmap $ fmap $ fmap f -- | Includes remote schema fields and actions buildActionQueryFields @@ -536,8 +543,8 @@ buildRelayPostgresQueryFields sourceName sourceConfig allTables (takeExposedAs F lift $ selectTableConnection table fieldName fieldDesc pkeyColumns selectPerms functionConnectionFields <- for queryFunctions $ \function -> runMaybeT do - let returnTable = fiReturnType function - functionName = fiName function + let returnTable = _fiReturnType function + functionName = _fiName function pkeyColumns <- MaybeT $ (^? tiCoreInfo.tciPrimaryKey._Just.pkColumns) <$> askTableInfo returnTable selectPerms <- MaybeT $ tableSelectPermissions returnTable @@ -762,24 +769,34 @@ buildMutationParser -> m (Maybe (Parser 'Output n (OMap.InsOrdHashMap G.Name (MutationRootField (UnpreparedValue 'Postgres))))) buildMutationParser allRemotes allActions nonObjectCustomTypes (takeExposedAs FEAMutation fst -> mutationFunctions) pgMutationFields = do - + roleName <- askRoleName + functionPermsCtx <- asks $ qcFunctionPermsContext . getter -- NOTE: this is basically copied from functionSelectExpParsers body - functionMutationExpParsers <- for mutationFunctions \(function@FunctionInfo{..}, (sourceName, sourceConfig)) -> do - selectPerms <- tableSelectPermissions fiReturnType - for selectPerms \perms -> do - displayName <- PG.qualifiedObjectToName fiName - let functionDesc = G.Description $ - "execute VOLATILE function " <> fiName <<> " which returns " <>> fiReturnType - asDbRootField = - let pgExecCtx = PG._pscExecCtx sourceConfig - in RFDB sourceName pgExecCtx + functionMutationExpParsers <- + case functionPermsCtx of + -- when function permissions are inferred, we don't expose the + -- mutation functions. See Note [Function Permissions] + FunctionPermissionsInferred -> pure [] + FunctionPermissionsManual -> + for mutationFunctions \(function@FunctionInfo{..}, (sourceName, sourceConfig)) -> runMaybeT do + selectPerms <- lift $ tableSelectPermissions _fiReturnType + -- A function exposed as mutation must have a function permission + -- configured for the role. See Note [Function Permissions] + guard $ roleName == adminRoleName || roleName `elem` _fiPermissions + perms <- hoistMaybe selectPerms + displayName <- PG.qualifiedObjectToName _fiName + let functionDesc = G.Description $ + "execute VOLATILE function " <> _fiName <<> " which returns " <>> _fiReturnType + asDbRootField = + let pgExecCtx = PG._pscExecCtx sourceConfig + in RFDB sourceName pgExecCtx - catMaybes <$> sequenceA - [ requiredFieldParser (asDbRootField . MDBFunction) $ - selectFunction function displayName (Just functionDesc) perms - -- FWIW: The equivalent of this is possible for mutations; do we want that?: - -- , mapMaybeFieldParser (asDbRootField . QDBAggregation) $ selectFunctionAggregate function aggName (Just aggDesc) perms - ] + catMaybes <$> sequenceA + [ requiredFieldParser (asDbRootField . MDBFunction) $ + lift $ selectFunction function displayName (Just functionDesc) perms + -- FWIW: The equivalent of this is possible for mutations; do we want that?: + -- , mapMaybeFieldParser (asDbRootField . QDBAggregation) $ selectFunctionAggregate function aggName (Just aggDesc) perms + ] actionParsers <- for allActions $ \actionInfo -> case _adType (_aiDefinition actionInfo) of @@ -804,7 +821,7 @@ buildMutationParser allRemotes allActions nonObjectCustomTypes -- local helpers takeExposedAs :: FunctionExposedAs -> (a -> FunctionInfo b) -> [a] -> [a] -takeExposedAs x f = filter ((== x) . fiExposedAs . f) +takeExposedAs x f = filter ((== x) . _fiExposedAs . f) subscriptionRoot :: G.Name subscriptionRoot = $$(G.litName "subscription_root") diff --git a/server/src-lib/Hasura/GraphQL/Schema/Common.hs b/server/src-lib/Hasura/GraphQL/Schema/Common.hs index d60f051b01a..54dc4d70872 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Common.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Common.hs @@ -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 diff --git a/server/src-lib/Hasura/GraphQL/Schema/Select.hs b/server/src-lib/Hasura/GraphQL/Schema/Select.hs index 8ef5ec3c625..7f028c74bc2 100644 --- a/server/src-lib/Hasura/GraphQL/Schema/Select.hs +++ b/server/src-lib/Hasura/GraphQL/Schema/Select.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DDL/EventTrigger.hs b/server/src-lib/Hasura/RQL/DDL/EventTrigger.hs index 56a283a9aec..599afd28c66 100644 --- a/server/src-lib/Hasura/RQL/DDL/EventTrigger.hs +++ b/server/src-lib/Hasura/RQL/DDL/EventTrigger.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DDL/Metadata.hs b/server/src-lib/Hasura/RQL/DDL/Metadata.hs index d310caf1672..75e5394e901 100644 --- a/server/src-lib/Hasura/RQL/DDL/Metadata.hs +++ b/server/src-lib/Hasura/RQL/DDL/Metadata.hs @@ -20,7 +20,7 @@ import qualified Data.HashMap.Strict.InsOrd as OMap import qualified Data.HashSet as HS import qualified Data.List as L -import Control.Lens ((^?)) +import Control.Lens ((.~), (^?)) import Data.Aeson import Hasura.Metadata.Class @@ -108,11 +108,38 @@ runReplaceMetadata replaceMetadata = do pure successMsg + runExportMetadata - :: (MetadataM m) + :: forall m . ( QErrM m, MetadataM m, HasServerConfigCtx m) => ExportMetadata -> m EncJSON -runExportMetadata _ = - AO.toEncJSON . metadataToOrdJSON <$> getMetadata +runExportMetadata _ = do + functionPermsCtx <- _sccFunctionPermsCtx <$> askServerConfigCtx + metadata <- getMetadata + exportMetadata <- processFunctionPermissions functionPermsCtx metadata + pure $ AO.toEncJSON . metadataToOrdJSON $ exportMetadata + where + -- | when FunctionPermissionsCtx is set to `FunctionPermissionsInferred` + -- we don't export the function permissions to the exported metadata + -- Note: Please **do not** make this function public as this is only meant + -- to be used while exporting metadata i.e. we don't intend on deleting + -- any function permissions that may exist in the DB, we simply hide it + -- from the user. + processFunctionPermissions :: FunctionPermissionsCtx -> Metadata -> m Metadata + processFunctionPermissions FunctionPermissionsManual metadata = pure metadata + processFunctionPermissions FunctionPermissionsInferred metadata = + let sources = OMap.keys $ _metaSources metadata + in foldrM clearFunctionPermission metadata sources + where + clearFunctionPermission sourceName accumulatedMetadata = do + let sourceFunctions = + OMap.keys . _smFunctions <$> OMap.lookup sourceName (_metaSources accumulatedMetadata) + functions <- onNothing sourceFunctions (throw500 "unexpected: runExportMetadata - source not found") + pure $ foldr + (\functionName md -> + ((metaSources.ix sourceName.smFunctions.ix functionName.fmPermissions .~ mempty) md)) + accumulatedMetadata + functions + runReloadMetadata :: (QErrM m, CacheRWM m, MetadataM m) => ReloadMetadata -> m EncJSON runReloadMetadata (ReloadMetadata reloadRemoteSchemas reloadSources) = do @@ -177,7 +204,8 @@ purgeMetadataObj = \case MTOTrigger trn -> dropEventTriggerInMetadata trn MTOComputedField ccn -> dropComputedFieldInMetadata ccn MTORemoteRelationship rn -> dropRemoteRelationshipInMetadata rn - SMOFunction qf -> dropFunctionInMetadata source qf + SMOFunction qf -> dropFunctionInMetadata source qf + SMOFunctionPermission qf rn -> dropFunctionPermissionInMetadata source qf rn MORemoteSchema rsn -> dropRemoteSchemaInMetadata rsn MORemoteSchemaPermissions rsName role -> dropRemoteSchemaPermissionInMetadata rsName role MOCustomTypes -> clearCustomTypesInMetadata diff --git a/server/src-lib/Hasura/RQL/DDL/Metadata/Generator.hs b/server/src-lib/Hasura/RQL/DDL/Metadata/Generator.hs index e5ab5bd2ac8..eaf3efc0d52 100644 --- a/server/src-lib/Hasura/RQL/DDL/Metadata/Generator.hs +++ b/server/src-lib/Hasura/RQL/DDL/Metadata/Generator.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DDL/RemoteSchema.hs b/server/src-lib/Hasura/RQL/DDL/RemoteSchema.hs index 4c763f30424..ddee56a189a 100644 --- a/server/src-lib/Hasura/RQL/DDL/RemoteSchema.hs +++ b/server/src-lib/Hasura/RQL/DDL/RemoteSchema.hs @@ -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 " diff --git a/server/src-lib/Hasura/RQL/DDL/Schema.hs b/server/src-lib/Hasura/RQL/DDL/Schema.hs index a223c2c3f9d..f1920250fe9 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema.hs @@ -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] diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs index 1c8306331b2..7c71f155388 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Cache/Common.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Cache/Common.hs index 58f8b2e1545..d6326b214e8 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Cache/Common.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Cache/Common.hs @@ -117,10 +117,9 @@ $(makeLenses ''BuildOutputs) -- | Parameters required for schema cache build data CacheBuildParams = CacheBuildParams - { _cbpManager :: !HTTP.Manager - , _cbpSqlGenCtx :: !SQLGenCtx - , _cbpRemoteSchemaPermsCtx :: !RemoteSchemaPermsCtx - , _cbpSourceResolver :: !SourceResolver + { _cbpManager :: !HTTP.Manager + , _cbpSourceResolver :: !SourceResolver + , _cbpServerConfigCtx :: !ServerConfigCtx } -- | The monad in which @'RebuildableSchemaCache' is being run @@ -138,11 +137,8 @@ newtype CacheBuild a instance HTTP.HasHttpManagerM CacheBuild where askHttpManager = asks _cbpManager -instance HasSQLGenCtx CacheBuild where - askSQLGenCtx = asks _cbpSqlGenCtx - -instance HasRemoteSchemaPermsCtx CacheBuild where - askRemoteSchemaPermsCtx = asks _cbpRemoteSchemaPermsCtx +instance HasServerConfigCtx CacheBuild where + askServerConfigCtx = asks _cbpServerConfigCtx instance MonadResolveSource CacheBuild where getSourceResolver = asks _cbpSourceResolver @@ -160,17 +156,15 @@ runCacheBuildM :: ( MonadIO m , MonadError QErr m , HTTP.HasHttpManagerM m - , HasSQLGenCtx m - , HasRemoteSchemaPermsCtx m + , HasServerConfigCtx m , MonadResolveSource m ) => CacheBuild a -> m a runCacheBuildM m = do params <- CacheBuildParams <$> HTTP.askHttpManager - <*> askSQLGenCtx - <*> askRemoteSchemaPermsCtx <*> getSourceResolver + <*> askServerConfigCtx runCacheBuild params m data RebuildableSchemaCache diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Cache/Dependencies.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Cache/Dependencies.hs index 64e3108fd0b..3f5649e7bce 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Cache/Dependencies.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Cache/Dependencies.hs @@ -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) diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Function.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Function.hs index ee0c2e99750..e1aa0c76289 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Function.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Function.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/LegacyCatalog.hs b/server/src-lib/Hasura/RQL/DDL/Schema/LegacyCatalog.hs index 3be5656e2d9..bfad846c3c6 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/LegacyCatalog.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/LegacyCatalog.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DML/Delete.hs b/server/src-lib/Hasura/RQL/DML/Delete.hs index 365ce0934eb..fb5867803d2 100644 --- a/server/src-lib/Hasura/RQL/DML/Delete.hs +++ b/server/src-lib/Hasura/RQL/DML/Delete.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DML/Insert.hs b/server/src-lib/Hasura/RQL/DML/Insert.hs index 6832e63feed..83e0ad01d29 100644 --- a/server/src-lib/Hasura/RQL/DML/Insert.hs +++ b/server/src-lib/Hasura/RQL/DML/Insert.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DML/Internal.hs b/server/src-lib/Hasura/RQL/DML/Internal.hs index 8217e178088..807a850a0cd 100644 --- a/server/src-lib/Hasura/RQL/DML/Internal.hs +++ b/server/src-lib/Hasura/RQL/DML/Internal.hs @@ -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) diff --git a/server/src-lib/Hasura/RQL/DML/Select.hs b/server/src-lib/Hasura/RQL/DML/Select.hs index 1ec169ecf6a..2b8540e9c02 100644 --- a/server/src-lib/Hasura/RQL/DML/Select.hs +++ b/server/src-lib/Hasura/RQL/DML/Select.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/DML/Update.hs b/server/src-lib/Hasura/RQL/DML/Update.hs index be687980bdb..0b714d25e5c 100644 --- a/server/src-lib/Hasura/RQL/DML/Update.hs +++ b/server/src-lib/Hasura/RQL/DML/Update.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/Types.hs b/server/src-lib/Hasura/RQL/Types.hs index 692ebf154d0..6213b59dcb8 100644 --- a/server/src-lib/Hasura/RQL/Types.hs +++ b/server/src-lib/Hasura/RQL/Types.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/Types/Function.hs b/server/src-lib/Hasura/RQL/Types/Function.hs index f744796661d..c2a127217c7 100644 --- a/server/src-lib/Hasura/RQL/Types/Function.hs +++ b/server/src-lib/Hasura/RQL/Types/Function.hs @@ -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 diff --git a/server/src-lib/Hasura/RQL/Types/Metadata.hs b/server/src-lib/Hasura/RQL/Types/Metadata.hs index 6bd50451537..9322a785650 100644 --- a/server/src-lib/Hasura/RQL/Types/Metadata.hs +++ b/server/src-lib/Hasura/RQL/Types/Metadata.hs @@ -51,6 +51,7 @@ instance Hashable TableMetadataObjId data SourceMetadataObjId = SMOTable !QualifiedTable | SMOFunction !QualifiedFunction + | SMOFunctionPermission !QualifiedFunction !RoleName | SMOTableObj !QualifiedTable !TableMetadataObjId deriving (Show, Eq, Generic) instance Hashable SourceMetadataObjId @@ -76,6 +77,7 @@ moiTypeName = \case MOSourceObjId _ sourceObjId -> case sourceObjId of SMOTable _ -> "table" SMOFunction _ -> "function" + SMOFunctionPermission _ _ -> "function_permission" SMOTableObj _ tableObjectId -> case tableObjectId of MTORel _ relType -> relTypeToTxt relType <> "_relation" MTOPerm _ permType -> permTypeToCode permType <> "_permission" @@ -96,6 +98,9 @@ moiName objectId = moiTypeName objectId <> " " <> case objectId of MOSourceObjId source sourceObjId -> case sourceObjId of SMOTable name -> toTxt name <> " in source " <> toTxt source SMOFunction name -> toTxt name <> " in source " <> toTxt source + SMOFunctionPermission functionName roleName -> + toTxt roleName <> " permission for function " + <> toTxt functionName <> " in source " <> toTxt source SMOTableObj tableName tableObjectId -> let tableObjectName = case tableObjectId of MTORel name _ -> toTxt name @@ -322,20 +327,30 @@ instance FromJSON TableMetadata where , cfKey, rrKey ] +newtype FunctionPermissionMetadata + = FunctionPermissionMetadata + { _fpmRole :: RoleName + } deriving (Show, Eq, Generic) +instance Cacheable FunctionPermissionMetadata +$(makeLenses ''FunctionPermissionMetadata) +$(deriveJSON hasuraJSON ''FunctionPermissionMetadata) + data FunctionMetadata = FunctionMetadata { _fmFunction :: !QualifiedFunction , _fmConfiguration :: !FunctionConfig + , _fmPermissions :: ![FunctionPermissionMetadata] } deriving (Show, Eq, Generic) instance Cacheable FunctionMetadata $(makeLenses ''FunctionMetadata) $(deriveToJSON hasuraJSON ''FunctionMetadata) instance FromJSON FunctionMetadata where - parseJSON = withObject "Object" $ \o -> + parseJSON = withObject "FunctionMetadata" $ \o -> FunctionMetadata <$> o .: "function" <*> o .:? "configuration" .!= emptyFunctionConfig + <*> o .:? "permissions" .!= [] type Tables = InsOrdHashMap QualifiedTable TableMetadata type Functions = InsOrdHashMap QualifiedFunction FunctionMetadata @@ -454,7 +469,7 @@ instance FromJSON MetadataNoSources where tables <- oMapFromL _tmTable <$> o .: "tables" functionList <- o .:? "functions" .!= [] let functions = OM.fromList $ flip map functionList $ - \function -> (function, FunctionMetadata function emptyFunctionConfig) + \function -> (function, FunctionMetadata function emptyFunctionConfig mempty) pure (tables, functions) MVVersion2 -> do tables <- oMapFromL _tmTable <$> o .: "tables" @@ -652,9 +667,13 @@ metadataToOrdJSON ( Metadata functionMetadataToOrdJSON :: FunctionMetadata -> AO.Value functionMetadataToOrdJSON FunctionMetadata{..} = - AO.object $ [("function", AO.toOrdered _fmFunction)] - <> if _fmConfiguration == emptyFunctionConfig then [] - else pure ("configuration", AO.toOrdered _fmConfiguration) + let confKeyPair = + if _fmConfiguration == emptyFunctionConfig then [] + else pure ("configuration", AO.toOrdered _fmConfiguration) + permissionsKeyPair = + if (null _fmPermissions) then [] + else pure ("permissions", AO.toOrdered _fmPermissions) + in AO.object $ [("function", AO.toOrdered _fmFunction)] <> confKeyPair <> permissionsKeyPair remoteSchemaQToOrdJSON :: RemoteSchemaMetadata -> AO.Value remoteSchemaQToOrdJSON (RemoteSchemaMetadata name definition comment permissions) = diff --git a/server/src-lib/Hasura/RQL/Types/Run.hs b/server/src-lib/Hasura/RQL/Types/Run.hs index b3eacca283b..d1467fa3aeb 100644 --- a/server/src-lib/Hasura/RQL/Types/Run.hs +++ b/server/src-lib/Hasura/RQL/Types/Run.hs @@ -24,10 +24,9 @@ import Hasura.Session data RunCtx = RunCtx - { _rcUserInfo :: !UserInfo - , _rcHttpMgr :: !HTTP.Manager - , _rcSqlGenCtx :: !SQLGenCtx - , _rcRemoteSchemaPermsCtx :: !RemoteSchemaPermsCtx + { _rcUserInfo :: !UserInfo + , _rcHttpMgr :: !HTTP.Manager + , _rcServerConfigCtx :: !ServerConfigCtx } newtype RunT m a @@ -54,11 +53,8 @@ instance (Monad m) => UserInfoM (RunT m) where instance (Monad m) => HTTP.HasHttpManagerM (RunT m) where askHttpManager = asks _rcHttpMgr -instance (Monad m) => HasSQLGenCtx (RunT m) where - askSQLGenCtx = asks _rcSqlGenCtx - -instance (Monad m) => HasRemoteSchemaPermsCtx (RunT m) where - askRemoteSchemaPermsCtx = asks _rcRemoteSchemaPermsCtx +instance (Monad m) => HasServerConfigCtx (RunT m) where + askServerConfigCtx = asks _rcServerConfigCtx instance (MonadResolveSource m) => MonadResolveSource (RunT m) where getSourceResolver = RunT . lift . lift $ getSourceResolver diff --git a/server/src-lib/Hasura/Server/API/Metadata.hs b/server/src-lib/Hasura/Server/API/Metadata.hs index 26690a37b4c..fd7f0011966 100644 --- a/server/src-lib/Hasura/Server/API/Metadata.hs +++ b/server/src-lib/Hasura/Server/API/Metadata.hs @@ -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 diff --git a/server/src-lib/Hasura/Server/API/Query.hs b/server/src-lib/Hasura/Server/API/Query.hs index dd9a77426fa..0c251d22e2b 100644 --- a/server/src-lib/Hasura/Server/API/Query.hs +++ b/server/src-lib/Hasura/Server/API/Query.hs @@ -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 diff --git a/server/src-lib/Hasura/Server/API/V2Query.hs b/server/src-lib/Hasura/Server/API/V2Query.hs index d31d33d605f..9b09bd1445f 100644 --- a/server/src-lib/Hasura/Server/API/V2Query.hs +++ b/server/src-lib/Hasura/Server/API/V2Query.hs @@ -66,9 +66,10 @@ runQuery -> HTTP.Manager -> SQLGenCtx -> RemoteSchemaPermsCtx + -> FunctionPermissionsCtx -> RQLQuery -> m (EncJSON, RebuildableSchemaCache) -runQuery env instanceId userInfo schemaCache httpManager sqlGenCtx remoteSchemaPermCtx rqlQuery = do +runQuery env instanceId userInfo schemaCache httpManager sqlGenCtx remoteSchemaPermCtx functionPermsCtx rqlQuery = do metadata <- fetchMetadata result <- runQueryM env rqlQuery & Tracing.interpTraceT \x -> do (((js, tracemeta), meta), rsc, ci) <- @@ -80,7 +81,8 @@ runQuery env instanceId userInfo schemaCache httpManager sqlGenCtx remoteSchemaP pure ((js, rsc, ci, meta), tracemeta) withReload result where - runCtx = RunCtx userInfo httpManager sqlGenCtx remoteSchemaPermCtx + runCtx = RunCtx userInfo httpManager + $ ServerConfigCtx functionPermsCtx remoteSchemaPermCtx sqlGenCtx withReload (result, updatedCache, invalidations, updatedMetadata) = do when (queryModifiesSchema rqlQuery) $ do @@ -92,9 +94,9 @@ runQuery env instanceId userInfo schemaCache httpManager sqlGenCtx remoteSchemaP queryModifiesSchema :: RQLQuery -> Bool queryModifiesSchema = \case - RQRunSql q -> isSchemaCacheBuildRequiredRunSQL q - RQBulk l -> any queryModifiesSchema l - _ -> False + RQRunSql q -> isSchemaCacheBuildRequiredRunSQL q + RQBulk l -> any queryModifiesSchema l + _ -> False runQueryM :: ( HasVersion @@ -103,7 +105,7 @@ runQueryM , MonadBaseControl IO m , UserInfoM m , CacheRWM m - , HasSQLGenCtx m + , HasServerConfigCtx m , Tracing.MonadTrace m , MetadataM m ) diff --git a/server/src-lib/Hasura/Server/App.hs b/server/src-lib/Hasura/Server/App.hs index 11abfd8e5d3..913b9591cb5 100644 --- a/server/src-lib/Hasura/Server/App.hs +++ b/server/src-lib/Hasura/Server/App.hs @@ -113,6 +113,7 @@ data ServerCtx , scResponseInternalErrorsConfig :: !ResponseInternalErrorsConfig , scEnvironment :: !Env.Environment , scRemoteSchemaPermsCtx :: !RemoteSchemaPermsCtx + , scFunctionPermsCtx :: !FunctionPermissionsCtx } data HandlerCtx @@ -391,15 +392,16 @@ v1QueryHandler query = do return $ HttpResponse res [] where action = do - userInfo <- asks hcUser - scRef <- asks (scCacheRef . hcServerCtx) - schemaCache <- fmap fst $ liftIO $ readIORef $ _scrCache scRef - httpMgr <- asks (scManager . hcServerCtx) - sqlGenCtx <- asks (scSQLGenCtx . hcServerCtx) - instanceId <- asks (scInstanceId . hcServerCtx) - env <- asks (scEnvironment . hcServerCtx) + userInfo <- asks hcUser + scRef <- asks (scCacheRef . hcServerCtx) + schemaCache <- fmap fst $ liftIO $ readIORef $ _scrCache scRef + httpMgr <- asks (scManager . hcServerCtx) + sqlGenCtx <- asks (scSQLGenCtx . hcServerCtx) + instanceId <- asks (scInstanceId . hcServerCtx) + env <- asks (scEnvironment . hcServerCtx) remoteSchemaPermsCtx <- asks (scRemoteSchemaPermsCtx . hcServerCtx) - runQuery env instanceId userInfo schemaCache httpMgr sqlGenCtx remoteSchemaPermsCtx query + functionPermsCtx <- asks (scFunctionPermsCtx . hcServerCtx) + runQuery env instanceId userInfo schemaCache httpMgr sqlGenCtx remoteSchemaPermsCtx functionPermsCtx query v1MetadataHandler :: ( HasVersion @@ -414,17 +416,19 @@ v1MetadataHandler => RQLMetadata -> m (HttpResponse EncJSON) v1MetadataHandler query = do (liftEitherM . authorizeV1MetadataApi query) =<< ask - userInfo <- asks hcUser - scRef <- asks (scCacheRef . hcServerCtx) - schemaCache <- fmap fst $ liftIO $ readIORef $ _scrCache scRef - httpMgr <- asks (scManager . hcServerCtx) - sqlGenCtx <- asks (scSQLGenCtx . hcServerCtx) - env <- asks (scEnvironment . hcServerCtx) - instanceId <- asks (scInstanceId . hcServerCtx) - logger <- asks (scLogger . hcServerCtx) + userInfo <- asks hcUser + scRef <- asks (scCacheRef . hcServerCtx) + schemaCache <- fmap fst $ liftIO $ readIORef $ _scrCache scRef + httpMgr <- asks (scManager . hcServerCtx) + sqlGenCtx <- asks (scSQLGenCtx . hcServerCtx) + env <- asks (scEnvironment . hcServerCtx) + instanceId <- asks (scInstanceId . hcServerCtx) + logger <- asks (scLogger . hcServerCtx) remoteSchemaPermsCtx <- asks (scRemoteSchemaPermsCtx . hcServerCtx) + functionPermsCtx <- asks (scFunctionPermsCtx . hcServerCtx) + let serverConfigCtx = ServerConfigCtx functionPermsCtx remoteSchemaPermsCtx sqlGenCtx r <- withSCUpdate scRef logger $ - runMetadataQuery env instanceId userInfo httpMgr sqlGenCtx remoteSchemaPermsCtx schemaCache query + runMetadataQuery env instanceId userInfo httpMgr serverConfigCtx schemaCache query pure $ HttpResponse r [] v2QueryHandler @@ -453,7 +457,8 @@ v2QueryHandler query = do instanceId <- asks (scInstanceId . hcServerCtx) env <- asks (scEnvironment . hcServerCtx) remoteSchemaPermsCtx <- asks (scRemoteSchemaPermsCtx . hcServerCtx) - V2Q.runQuery env instanceId userInfo schemaCache httpMgr sqlGenCtx remoteSchemaPermsCtx query + functionPermsCtx <- asks (scFunctionPermsCtx . hcServerCtx) + V2Q.runQuery env instanceId userInfo schemaCache httpMgr sqlGenCtx remoteSchemaPermsCtx functionPermsCtx query v1Alpha1GQHandler :: ( HasVersion @@ -727,13 +732,14 @@ mkWaiApp -> RebuildableSchemaCache -> EKG.Store -> RemoteSchemaPermsCtx + -> FunctionPermissionsCtx -> WS.ConnectionOptions -> KeepAliveDelay -- ^ Metadata storage connection pool -> m HasuraApp mkWaiApp env logger sqlGenCtx enableAL httpManager mode corsCfg enableConsole consoleAssetsDir enableTelemetry instanceId apis lqOpts _ {- planCacheOptions -} responseErrorsConfig - liveQueryHook schemaCache ekgStore enableRSPermsCtx connectionOptions keepAliveDelay = do + liveQueryHook schemaCache ekgStore enableRSPermsCtx functionPermsCtx connectionOptions keepAliveDelay = do -- See Note [Temporarily disabling query plan caching] -- (planCache, schemaCacheRef) <- initialiseCache @@ -762,6 +768,7 @@ mkWaiApp env logger sqlGenCtx enableAL httpManager mode corsCfg enableConsole co , scEnvironment = env , scResponseInternalErrorsConfig = responseErrorsConfig , scRemoteSchemaPermsCtx = enableRSPermsCtx + , scFunctionPermsCtx = functionPermsCtx } spockApp <- liftWithStateless $ \lowerIO -> diff --git a/server/src-lib/Hasura/Server/Init.hs b/server/src-lib/Hasura/Server/Init.hs index 737a5b9ed6c..794069b554c 100644 --- a/server/src-lib/Hasura/Server/Init.hs +++ b/server/src-lib/Hasura/Server/Init.hs @@ -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. diff --git a/server/src-lib/Hasura/Server/Init/Config.hs b/server/src-lib/Hasura/Server/Init/Config.hs index 30c33aae657..8ba367b6fd1 100644 --- a/server/src-lib/Hasura/Server/Init/Config.hs +++ b/server/src-lib/Hasura/Server/Init/Config.hs @@ -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 diff --git a/server/src-lib/Hasura/Server/SchemaUpdate.hs b/server/src-lib/Hasura/Server/SchemaUpdate.hs index d83c038e5a0..3832870d10b 100644 --- a/server/src-lib/Hasura/Server/SchemaUpdate.hs +++ b/server/src-lib/Hasura/Server/SchemaUpdate.hs @@ -82,7 +82,7 @@ data SchemaSyncEvent instance ToJSON SchemaSyncEvent where toJSON = \case SSEListenStart time -> String $ "event listening started at " <> tshow time - SSEPayload payload -> toJSON payload + SSEPayload payload -> toJSON payload data ThreadError = TEPayloadParse !Text @@ -192,12 +192,14 @@ startSchemaSyncProcessorThread -> InstanceId -> UTC.UTCTime -> RemoteSchemaPermsCtx + -> FunctionPermissionsCtx -> ManagedT m Immortal.Thread startSchemaSyncProcessorThread sqlGenCtx logger httpMgr - schemaSyncEventRef cacheRef instanceId cacheInitStartTime remoteSchemaPermsCtx = do + schemaSyncEventRef cacheRef instanceId cacheInitStartTime remoteSchemaPermsCtx functionPermsCtx = do -- Start processor thread processorThread <- C.forkManagedT "SchemeUpdate.processor" logger $ - processor sqlGenCtx logger httpMgr schemaSyncEventRef cacheRef instanceId cacheInitStartTime remoteSchemaPermsCtx + processor sqlGenCtx logger httpMgr schemaSyncEventRef + cacheRef instanceId cacheInitStartTime remoteSchemaPermsCtx functionPermsCtx logThreadStarted logger instanceId TTProcessor processorThread pure processorThread @@ -258,9 +260,10 @@ processor -> InstanceId -> UTC.UTCTime -> RemoteSchemaPermsCtx + -> FunctionPermissionsCtx -> m void processor sqlGenCtx logger httpMgr updateEventRef - cacheRef instanceId cacheInitStartTime remoteSchemaPermsCtx = + cacheRef instanceId cacheInitStartTime remoteSchemaPermsCtx functionPermsCtx = -- Never exits forever $ do event <- liftIO $ STM.atomically getLatestEvent @@ -281,7 +284,7 @@ processor sqlGenCtx logger httpMgr updateEventRef when shouldReload $ refreshSchemaCache sqlGenCtx logger httpMgr cacheRef cacheInvalidations - threadType remoteSchemaPermsCtx "schema cache reloaded" + threadType remoteSchemaPermsCtx functionPermsCtx "schema cache reloaded" where -- checks if there is an event -- and replaces it with Nothing @@ -307,10 +310,11 @@ refreshSchemaCache -> CacheInvalidations -> ThreadType -> RemoteSchemaPermsCtx + -> FunctionPermissionsCtx -> Text -> m () refreshSchemaCache sqlGenCtx logger httpManager - cacheRef invalidations threadType remoteSchemaPermsCtx msg = do + cacheRef invalidations threadType remoteSchemaPermsCtx functionPermsCtx msg = do -- Reload schema cache from catalog eitherMetadata <- runMetadataStorageT fetchMetadata resE <- runExceptT $ do @@ -325,7 +329,8 @@ refreshSchemaCache sqlGenCtx logger httpManager Left e -> logError logger threadType $ TEQueryError e Right () -> logInfo logger threadType $ object ["message" .= msg] where - runCtx = RunCtx adminUserInfo httpManager sqlGenCtx remoteSchemaPermsCtx + serverConfigCtx = ServerConfigCtx functionPermsCtx remoteSchemaPermsCtx sqlGenCtx + runCtx = RunCtx adminUserInfo httpManager serverConfigCtx logInfo :: (MonadIO m) => Logger Hasura -> ThreadType -> Value -> m () logInfo logger threadType val = unLogger logger $ diff --git a/server/src-lib/Hasura/Server/Telemetry.hs b/server/src-lib/Hasura/Server/Telemetry.hs index 45f1272ac3f..68838e4bc9a 100644 --- a/server/src-lib/Hasura/Server/Telemetry.hs +++ b/server/src-lib/Hasura/Server/Telemetry.hs @@ -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{..} diff --git a/server/src-test/Hasura/Server/MigrateSpec.hs b/server/src-test/Hasura/Server/MigrateSpec.hs index 308e0676b79..eca7454f98a 100644 --- a/server/src-test/Hasura/Server/MigrateSpec.hs +++ b/server/src-test/Hasura/Server/MigrateSpec.hs @@ -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 diff --git a/server/src-test/Main.hs b/server/src-test/Main.hs index 6b4d6e78e6d..41d22250698 100644 --- a/server/src-test/Main.hs +++ b/server/src-test/Main.hs @@ -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 = diff --git a/server/tests-py/conftest.py b/server/tests-py/conftest.py index 2bb95ca9c37..6045c8c0621 100644 --- a/server/tests-py/conftest.py +++ b/server/tests-py/conftest.py @@ -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)) diff --git a/server/tests-py/context.py b/server/tests-py/context.py index ddb566a50a2..2a2659726b4 100644 --- a/server/tests-py/context.py +++ b/server/tests-py/context.py @@ -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() diff --git a/server/tests-py/queries/graphql_mutation/functions/create_function_permission_add_to_score.yaml b/server/tests-py/queries/graphql_mutation/functions/create_function_permission_add_to_score.yaml new file mode 100644 index 00000000000..134530a897c --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/functions/create_function_permission_add_to_score.yaml @@ -0,0 +1,4 @@ +type: pg_create_function_permission +args: + function: add_to_score + role: anonymous diff --git a/server/tests-py/queries/graphql_mutation/functions/drop_function_permission_add_to_score.yaml b/server/tests-py/queries/graphql_mutation/functions/drop_function_permission_add_to_score.yaml new file mode 100644 index 00000000000..635b6f7b1c8 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/functions/drop_function_permission_add_to_score.yaml @@ -0,0 +1,4 @@ +type: pg_drop_function_permission +args: + function: add_to_score + role: anonymous diff --git a/server/tests-py/queries/graphql_mutation/functions/function_without_function_permission.yaml b/server/tests-py/queries/graphql_mutation/functions/function_without_function_permission.yaml new file mode 100644 index 00000000000..fa8fd978c40 --- /dev/null +++ b/server/tests-py/queries/graphql_mutation/functions/function_without_function_permission.yaml @@ -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 diff --git a/server/tests-py/queries/graphql_query/functions/permissions/add_function_permission_get_articles.yaml b/server/tests-py/queries/graphql_query/functions/permissions/add_function_permission_get_articles.yaml new file mode 100644 index 00000000000..feed7909f42 --- /dev/null +++ b/server/tests-py/queries/graphql_query/functions/permissions/add_function_permission_get_articles.yaml @@ -0,0 +1,4 @@ +type: pg_create_function_permission +args: + role: user + function: get_articles diff --git a/server/tests-py/queries/graphql_query/functions/permissions/get_articles_with_permission_configured.yaml b/server/tests-py/queries/graphql_query/functions/permissions/get_articles_with_permission_configured.yaml new file mode 100644 index 00000000000..1b7cc9c9285 --- /dev/null +++ b/server/tests-py/queries/graphql_query/functions/permissions/get_articles_with_permission_configured.yaml @@ -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 + } + } diff --git a/server/tests-py/queries/graphql_query/functions/permissions/get_articles_without_permission_configured.yaml b/server/tests-py/queries/graphql_query/functions/permissions/get_articles_without_permission_configured.yaml new file mode 100644 index 00000000000..cf7830f652e --- /dev/null +++ b/server/tests-py/queries/graphql_query/functions/permissions/get_articles_without_permission_configured.yaml @@ -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 + } + } diff --git a/server/tests-py/queries/graphql_query/functions/permissions/setup.yaml b/server/tests-py/queries/graphql_query/functions/permissions/setup.yaml new file mode 100644 index 00000000000..2df83aec1aa --- /dev/null +++ b/server/tests-py/queries/graphql_query/functions/permissions/setup.yaml @@ -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 diff --git a/server/tests-py/queries/graphql_query/functions/permissions/teardown.yaml b/server/tests-py/queries/graphql_query/functions/permissions/teardown.yaml new file mode 100644 index 00000000000..0189948664c --- /dev/null +++ b/server/tests-py/queries/graphql_query/functions/permissions/teardown.yaml @@ -0,0 +1,9 @@ +type: bulk +args: + +- type: run_sql + args: + sql: | + DROP TABLE article cascade; + DROP TABLE author cascade; + cascade: true diff --git a/server/tests-py/test_graphql_mutations.py b/server/tests-py/test_graphql_mutations.py index 2e9da8792ef..3853a1b9464 100644 --- a/server/tests-py/test_graphql_mutations.py +++ b/server/tests-py/test_graphql_mutations.py @@ -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 diff --git a/server/tests-py/test_graphql_queries.py b/server/tests-py/test_graphql_queries.py index 64cf22b46c5..ebc3b5ae593 100644 --- a/server/tests-py/test_graphql_queries.py +++ b/server/tests-py/test_graphql_queries.py @@ -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')