server: add tests ensuring the correct functioning of all endpoints based on user roles

https://github.com/hasura/graphql-engine-mono/pull/1625

Co-authored-by: Sameer Kolhar <6604943+kolharsam@users.noreply.github.com>
GitOrigin-RevId: 6b56efc838d2ed1acc44b2847161fde22d6aee17
This commit is contained in:
Solomon Bothwell 2021-07-16 09:08:23 -07:00 committed by hasura-bot
parent 08847566d8
commit d88e2bbcce
33 changed files with 1087 additions and 95 deletions

View File

@ -33,3 +33,4 @@ insecure-webhook-with-admin-secret
allowlist-queries
jwk-url
horizontal-scaling
developer-api-tests

View File

@ -219,6 +219,7 @@ export HASURA_GRAPHQL_STRINGIFY_NUMERIC_TYPES=true
export REMOTE_SCHEMAS_WEBHOOK_DOMAIN="http://127.0.0.1:5000"
export PYTEST_ADDOPTS="-vv"
HGE_PIDS=""
WH_PID=""
WHC_PID=""
@ -274,6 +275,7 @@ case "$SERVER_TEST_TO_RUN" in
TEST_TYPE="admin-secret"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
start_multiple_hge_servers
@ -288,6 +290,7 @@ case "$SERVER_TEST_TO_RUN" in
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
export HASURA_GRAPHQL_UNAUTHORIZED_ROLE="anonymous"
run_hge_with_args serve
@ -306,7 +309,7 @@ case "$SERVER_TEST_TO_RUN" in
init_jwt
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key }')"
start_multiple_hge_servers
@ -325,6 +328,7 @@ case "$SERVER_TEST_TO_RUN" in
init_jwt
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_format: "stringified_json"}')"
run_hge_with_args serve
@ -343,6 +347,7 @@ case "$SERVER_TEST_TO_RUN" in
init_jwt
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , audience: "myapp-1234"}')"
run_hge_with_args serve
@ -363,6 +368,7 @@ case "$SERVER_TEST_TO_RUN" in
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , audience: ["myapp-1234", "myapp-9876"]}')"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
run_hge_with_args serve
wait_for_port 8080
@ -381,6 +387,7 @@ case "$SERVER_TEST_TO_RUN" in
init_jwt
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , issuer: "https://hasura.com"}')"
run_hge_with_args serve
@ -404,6 +411,7 @@ case "$SERVER_TEST_TO_RUN" in
# hasura claims at one level of nesting
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_namespace_path: "$.hasura_claims"}')"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
run_hge_with_args serve
wait_for_port 8080
@ -417,6 +425,7 @@ case "$SERVER_TEST_TO_RUN" in
# hasura claims at two levels of nesting with claims_namespace_path containing special character
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_namespace_path: "$.hasura['\''claims%'\'']"}')"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
run_hge_with_args serve
wait_for_port 8080
@ -430,6 +439,7 @@ case "$SERVER_TEST_TO_RUN" in
# hasura claims at the root of the JWT token
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_namespace_path: "$"}')"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
run_hge_with_args serve
wait_for_port 8080
@ -450,6 +460,7 @@ case "$SERVER_TEST_TO_RUN" in
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_map: {"x-hasura-user-id": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].user.id"}, "x-hasura-allowed-roles": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].role.allowed"}, "x-hasura-default-role": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].role.default"}}}')"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
run_hge_with_args serve
wait_for_port 8080
@ -465,6 +476,7 @@ case "$SERVER_TEST_TO_RUN" in
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_map: {"x-hasura-user-id": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].user.id", "default":"1"}, "x-hasura-allowed-roles": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].role.allowed", "default":["user","editor"]}, "x-hasura-default-role": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].role.default","default":"user"}}}')"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
run_hge_with_args serve
wait_for_port 8080
@ -483,6 +495,7 @@ case "$SERVER_TEST_TO_RUN" in
init_jwt
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , allowed_skew: 60}')"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
run_hge_with_args serve
wait_for_port 8080
@ -503,6 +516,7 @@ case "$SERVER_TEST_TO_RUN" in
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , claims_map: {"x-hasura-user-id": {"path":"$.['"'"'https://myapp.com/jwt/claims'"'"'].user.id"}, "x-hasura-allowed-roles": ["user","editor"], "x-hasura-default-role": "user","x-hasura-custom-header":"custom-value"}}')"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
run_hge_with_args serve
wait_for_port 8080
@ -523,6 +537,7 @@ case "$SERVER_TEST_TO_RUN" in
export HASURA_GRAPHQL_JWT_SECRET="$(jq -n --arg key "$(cat $OUTPUT_FOLDER/ssl/jwt_public.key)" '{ type: "RS512", key: $key , header: {"type": "Cookie", "name": "hasura_user"}}')"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
run_hge_with_args serve
wait_for_port 8080
@ -539,6 +554,7 @@ case "$SERVER_TEST_TO_RUN" in
echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH CORS DOMAINS ########>\n"
export HASURA_GRAPHQL_CORS_DOMAIN="http://*.localhost, http://localhost:3000, https://*.foo.bar.com"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
TEST_TYPE="cors-domains"
run_hge_with_args serve
@ -575,6 +591,7 @@ case "$SERVER_TEST_TO_RUN" in
ws-init-cookie-noread)
echo "$(time_elapsed): testcase 2: no read cookie, cors disabled"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
TEST_TYPE="ws-init-cookie-noread"
export HASURA_GRAPHQL_AUTH_HOOK="http://localhost:9876/auth"
export HASURA_GRAPHQL_AUTH_HOOK_MODE="POST"
@ -593,6 +610,7 @@ case "$SERVER_TEST_TO_RUN" in
ws-init-cookie-read-cors-disabled)
echo "$(time_elapsed): testcase 3: read cookie, cors disabled and ws-read-cookie"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
TEST_TYPE="ws-init-cookie-read-cors-disabled"
export HASURA_GRAPHQL_AUTH_HOOK="http://localhost:9876/auth"
export HASURA_GRAPHQL_AUTH_HOOK_MODE="POST"
@ -925,6 +943,22 @@ case "$SERVER_TEST_TO_RUN" in
kill_hge_servers
# end allowlist queries test
;;
developer-api-tests)
echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH DEVELOPER API ENABLED ########>\n"
TEST_TYPE="developer-api-tests"
export HASURA_GRAPHQL_ENABLED_APIS="metadata,graphql,developer,config,pgdump"
export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM"
run_hge_with_args serve --enabled-apis "$HASURA_GRAPHQL_ENABLED_APIS"
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-developer-api-enabled test_dev_endpoints.py
unset HASURA_GRAPHQL_ENABLED_APIS
kill_hge_servers
;;
jwk-url)
# TODO(swann): ditto, these have to be parallelised

View File

@ -41,7 +41,6 @@ import Network.Mime (defaultMimeLookup)
import System.FilePath (joinPath, takeFileName)
import Web.Spock.Core ((<//>))
import qualified Hasura.Backends.Postgres.SQL.Types as PG
import qualified Hasura.GraphQL.Execute as E
import qualified Hasura.GraphQL.Execute.LiveQuery.Options as EL
import qualified Hasura.GraphQL.Execute.LiveQuery.Poll as EL
@ -672,41 +671,6 @@ renderHtmlTemplate template jVal =
errMsg = "template rendering failed: " ++ show errs
(errs, res) = M.checkedSubstitute template jVal
newtype LegacyQueryParser m
= LegacyQueryParser
{ getLegacyQueryParser :: PG.QualifiedTable -> Object -> m RQLQueryV1 }
queryParsers :: (MonadError QErr m) => M.HashMap Text (LegacyQueryParser m)
queryParsers =
M.fromList
[ ("select", mkLegacyQueryParser RQSelect)
, ("insert", mkLegacyQueryParser RQInsert)
, ("update", mkLegacyQueryParser RQUpdate)
, ("delete", mkLegacyQueryParser RQDelete)
, ("count", mkLegacyQueryParser RQCount)
]
where
mkLegacyQueryParser f =
LegacyQueryParser $ \qt obj -> do
let val = Object $ M.insert "table" (toJSON qt) obj
q <- decodeValue val
return $ f q
legacyQueryHandler
:: ( HasVersion, MonadIO m, MonadBaseControl IO m, MonadMetadataApiAuthorization m, Tracing.MonadTrace m
, MonadReader HandlerCtx m
, MonadMetadataStorage m
, MonadResolveSource m
)
=> PG.TableName -> Text -> Object
-> m (HttpResponse EncJSON)
legacyQueryHandler tn queryType req =
case M.lookup queryType queryParsers of
Just queryParser -> getLegacyQueryParser queryParser qt req >>= v1QueryHandler . RQV1
Nothing -> throw404 "No such resource exists"
where
qt = PG.QualifiedObject PG.publicSchema tn
-- | Default implementation of the 'MonadConfigApiHandler'
configApiGetHandler
:: forall m. (HasVersion, MonadIO m, MonadBaseControl IO m, UserAuthentication (Tracing.TraceT m), HttpLog m, Tracing.HasReporter m, HasResourceLimits m)
@ -981,12 +945,6 @@ httpApp setupHook corsCfg serverCtx enableConsole consoleAssetsDir enableTelemet
Spock.post "v2/query" $ spockAction encodeQErr id $
mkPostHandler $ fmap (emptyHttpLogMetadata @m, ) <$> mkAPIRespHandler v2QueryHandler
Spock.post ("api/1/table" <//> Spock.var <//> Spock.var) $ \tableName queryType ->
mkSpockAction serverCtx encodeQErr id $
mkPostHandler $
fmap (emptyHttpLogMetadata @m, )
<$> mkAPIRespHandler (legacyQueryHandler (PG.TableName tableName) queryType)
when enablePGDump $
Spock.post "v1alpha1/pg_dump" $ spockAction encodeQErr id $
mkPostHandler $ fmap (emptyHttpLogMetadata @m,) <$> v1Alpha1PGDumpHandler
@ -1022,7 +980,7 @@ httpApp setupHook corsCfg serverCtx enableConsole consoleAssetsDir enableTelemet
Spock.get "dev/plan_cache" $ spockAction encodeQErr id $
mkGetHandler $ do
onlyAdmin
respJ <- liftIO $ E.dumpPlanCache {- scPlanCache serverCtx -}
respJ <- liftIO E.dumpPlanCache {- scPlanCache serverCtx -}
return (emptyHttpLogMetadata @m, JSONResp $ HttpResponse (encJFromJValue respJ) [])
Spock.get "dev/subscriptions" $ spockAction encodeQErr id $
mkGetHandler $ do

View File

@ -187,6 +187,12 @@ This option may result in test failures if the schema has to change between the
help="Flag to specify if the pro tests are to be run"
)
parser.addoption(
"--test-developer-api-enabled", action="store_true",
help="Run Test cases with the Developer API Enabled",
default=False
)
#By default,

View File

@ -625,3 +625,14 @@ class HGECtx:
self.ws_client.teardown()
self.ws_client_v1alpha1.teardown()
self.ws_client_relay.teardown()
def v1GraphqlExplain(self, q, hdrs=None):
headers = {}
if hdrs != None:
headers = hdrs
if self.hge_key != None:
headers['X-Hasura-Admin-Secret'] = self.hge_key
resp = self.http.post(self.hge_url + '/v1/graphql/explain', json=q, headers=headers)
return resp.status_code, resp.json()

View File

@ -1,6 +1,8 @@
# Test output of pg_dump clean output.
# Including "SET check_function_bodies = false;" avoids https://github.com/hasura/graphql-engine-mono/issues/1007
descriptions: Execute pg_dump on public schema
headers:
X-Hasura-Role: admin
url: /v1alpha1/pg_dump
status: 200
query:

View File

@ -0,0 +1,11 @@
- description: call the new custom endpoint with the wrong role
url: /api/rest/simple
headers:
X-Hasura-Role: user
method: GET
status: 400
query:
response:
path: $.selectionSet.test_table
error: "field \"test_table\" not found in type: 'query_root'"
code: validation-failed

View File

@ -1,5 +1,7 @@
- description: call the new custom endpoint
url: /api/rest/simple
headers:
X-Hasura-Role: admin
method: GET
status: 200
query:

View File

@ -0,0 +1,27 @@
description: Simple GraphQL object query on author, excercising multiple operations
url: /v1alpha1/graphql
headers:
X-Hasura-Role: admin
status: 200
response:
data:
author:
- id: 1
name: Author 1
- id: 2
name: Author 2
query:
# https://graphql.org/learn/serving-over-http/#post-request
operationName: chooseThisOne
query: |
query ignoreThisOne {
author {
name
}
}
query chooseThisOne {
author {
id
name
}
}

View File

@ -0,0 +1,60 @@
- description: Other Users cannot see unpublished articles
url: /v1alpha1/graphql
status: 200
headers:
X-Hasura-Role: anonymous
response:
data:
article: []
query:
query: |
query {
article (
where: { is_published: {_eq: false}}
) {
id
title
content
is_published
author {
id
name
}
}
}
- description: Other Users can only see published articles
url: /v1/graphql
status: 200
headers:
X-Hasura-Role: anonymous
response:
data:
article:
- id: 2
title: Article 2
content: Sample article content 2
is_published: true
author:
id: 1
name: Author 1
- id: 3
title: Article 3
content: Sample article content 3
is_published: true
author:
id: 2
name: Author 2
query:
query: |
query {
article {
id
title
content
is_published
author {
id
name
}
}
}

View File

@ -1,5 +1,7 @@
- description: Clear metadata
url: /v1/metadata
headers:
X-Hasura-Role: admin
status: 200
response:
message: success
@ -9,6 +11,8 @@
- description: Check if metadata is cleared
url: /v1/query
headers:
X-Hasura-Role: admin
status: 200
# FIXME:- Using export_metadata will dump
# the source configuration dependent on --database-url

View File

@ -0,0 +1,490 @@
- description: Clear metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: clear_metadata
args: {}
- description: Check if metadata is cleared
url: /v1/query
headers:
X-Hasura-Role: user
status: 400
query:
type: export_metadata
args: {}
- description: Export metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: export_metadata
args: {}
- description: Replace metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: replace_metadata
args:
allow_inconsistent_metadata: true
metadata:
{
"version": 3,
"sources": [
{
"name": "default",
"kind": "postgres",
"tables": [],
"configuration": {
"connection_info": {
"database_url": "postgres://postgres:postgrespassword@127.0.0.1:10432/tenant1",
"pool_settings": {
"retries": 1,
"idle_timeout": 185,
"max_connections": 50
}
}
}
}
]
}
- description: Reload metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: reload_metadata
args:
reload_remote_schemas: true
- description: Get Inconsistent Metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: get_inconsistent_metadata
args: {}
- description: Drop Inconsistent Metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: drop_inconsistent_metadata
args: {}
- description: PG Add Source Metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: pg_add_source
args:
{
"name": "pg1",
"configuration": {
"connection_info": {
"database_url": {
"from_env": "DB_URL_ENV_VAR"
},
"pool_settings": {
"max_connections": 50,
"idle_timeout": 180,
"retries": 1,
"pool_timeout": 360,
"connection_lifetime": 600
},
"use_prepared_statements": true,
"isolation_level": "read-committed",
}
},
"replace_configuration": false
}
- description: PG Drop Source Metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: pg_drop_source
args:
name: pg1
cascade: true
- description: PG Rename Source Metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: rename_source
args:
name: pg1
new_name: pg2
- description: PG Track Table Metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: pg_track_table
args: {
"table": "author",
"source": "default",
"configuration": {
"custom_root_fields": {
"select": "Authors",
"select_by_pk": "Author",
"select_aggregate": "AuthorAggregate",
"insert": "AddAuthors",
"insert_one":"AddAuthor",
"update": "UpdateAuthors",
"update_by_pk": "UpdateAuthor",
"delete": "DeleteAuthors",
"delete_by_pk": "DeleteAuthor"
},
"custom_column_names": {
"id": "authorId"
}
}
}
- description: PG Untrack Table Metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: pg_untrack_table
args: {
"table": {
"schema": "public",
"name": "author"
},
"source": "default",
"cascade": true
}
- description: PG Set Table Is Enum Metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: pg_set_table_is_enum
args: {
"table": {
"schema": "public",
"name": "user_role"
},
"source": "default",
"is_enum": true
}
- description: PG Set Table Customization
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: pg_set_table_customization
args: {
"table": "author_details",
"source": "default",
"configuration": {
"identifier": "author",
"custom_root_fields": {
"select": "Authors",
"select_by_pk": "Author",
"select_aggregate": "AuthorAggregate",
"insert": "AddAuthors",
"insert_one":"AddAuthor",
"update": "UpdateAuthors",
"update_by_pk": "UpdateAuthor",
"delete": "DeleteAuthors",
"delete_by_pk": "DeleteAuthor"
},
"custom_column_names": {
"id": "authorId"
}
}
}
- description: PG Track Function
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: pg_track_function
args: {
"function": {
"schema": "public",
"name": "search_articles"
},
"source": "default",
"configuration": {
"session_argument": "hasura_session"
}
}
- description: PG Untrack Function
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: pg_untrack_function
args: {
"function": {
"schema": "public",
"name": "search_articles"
},
"source": "default"
}
- description: PG Create Function Permission
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: pg_create_function_permission
args: {
"function": "get_articles",
"source": "default",
"role": "user"
}
- description: PG Drop Function Permission
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: pg_drop_function_permission
args: {
"function": "get_articles",
"role": "user",
"source": "default"
}
- description: MSSQL Add Source Metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: mssql_add_source
args: {
"name": "mssql1",
"configuration": {
"connection_info": {
"connection_string": {
"from_env": "CONN_STRING_ENV_VAR"
},
"pool_settings": {
"max_connections": 50,
"idle_timeout": 180
}
}
}
}
- description: MSSQL Drop Source Metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: mssql_drop_source
args:
name: mssql1
- description: MSSQL Track Table
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: mssql_track_table
args: {
"table": "author",
"source": "default"
}
- description: MSSQL Untrack Table
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: mssql_untrack_table
args: {
"table": {
"schema": "dbo",
"name": "author"
},
"source": "default",
"cascade": true
}
- description: MSSQL Set Table Customization
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: mssql_set_table_customization
args: {
"table": "author_details",
"source": "default",
"configuration": {
"identifier": "author",
"custom_root_fields": {
"select": "Authors",
"select_aggregate": "AuthorAggregate",
},
"custom_column_names": {
"id": "authorId"
}
}
}
# TODO: Failing to parse `configuration.service_account`
# - description: BigQuery Add Source Metadata
# url: /v1/metadata
# headers:
# X-Hasura-Role: user
# status: 400
# response:
# path: $.args
# error: 'restricted access : admin only'
# code: access-denied
# query:
# type: bigquery_add_source
# args:
# {
# "name": "bigquery1",
# "configuration": {
# "service_account": "bigquery_service_account",
# "project_id": "bigquery_project_id",
# "datasets": "dataset1, dataset2"
# }
# }
- description: BigQuery Drop Source Metadata
url: /v1/metadata
headers:
X-Hasura-Role: user
status: 400
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: bigquery_drop_source
args:
name: bigquery1

View File

@ -0,0 +1,14 @@
url: /v2/query
status: 400
headers:
X-Hasura-Role: user
X-Hasura-User-Id: '1'
response:
path: $.args
error: 'restricted access : admin only'
code: access-denied
query:
type: run_sql
args:
sql: |
SELECT * from author

View File

@ -0,0 +1,21 @@
url: /v2/query
headers:
X-Hasura-Role: admin
status: 200
response:
result_type: TuplesOk
result:
-
- id
- name
-
- '1'
- Author 1
-
- '2'
- Author 2
query:
type: run_sql
args:
sql: |
SELECT * from author

View File

@ -31,4 +31,4 @@ query:
- name: author
columns:
- id
- name
- name

View File

@ -1,10 +1,11 @@
description: foo
url: /v1/query
status: 200
response:
- id: 1
name: Author 1
- id: 2
name: Author 2
- id: 1
name: Author 1
- id: 2
name: Author 2
query:
type: select
args:

View File

@ -0,0 +1,15 @@
url: /v1/query
headers:
x-hasura-role: user
status: 400
response:
code: permission-denied
error: role "user" does not have permission to select column "id"
path: $.args.columns[0]
query:
type: select
args:
table: author
columns:
- id
- name

View File

@ -0,0 +1,13 @@
url: /v1/query
headers:
x-hasura-role: user
status: 200
response:
- name: Author 1
- name: Author 2
query:
type: select
args:
table: author
columns:
- name

View File

@ -1,15 +1,14 @@
type: bulk
args:
#Author table
- type: run_sql
args:
sql: |
create table author(
id serial primary key,
name text unique
CREATE TABLE author (
id SERIAL PRIMARY KEY,
name TEXT UNIQUE
);
CREATE TABLE article (
id SERIAL PRIMARY KEY,
title TEXT,
@ -18,12 +17,14 @@ args:
is_published BOOLEAN,
published_on TIMESTAMP
);
insert into author (name)
values
INSERT INTO author (name)
VALUES
('Author 1'),
('Author 2') ;
insert into article (title,content,author_id,is_published)
values
('Author 2');
INSERT INTO article (title,content,author_id,is_published)
VALUES
(
'Article 1',
'Sample article content 1',
@ -42,6 +43,7 @@ args:
2,
true
);
CREATE TABLE "user" (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
@ -53,13 +55,25 @@ args:
schema: public
name: author
#Article table
- type: track_table
args:
schema: public
name: article
#Object relationship
- type: track_table
args:
schema: public
name: user
- type: insert
args:
table: user
objects:
- name: User 1
number: '123456789'
- name: User 2
number: '123456780'
- type: create_object_relationship
args:
table: article
@ -67,7 +81,6 @@ args:
using:
foreign_key_constraint_on: author_id
#Array relationship
- type: create_array_relationship
args:
table: author
@ -77,17 +90,11 @@ args:
table: article
column: author_id
#Insert values
- type: track_table
- type: create_select_permission
args:
schema: public
name: user
- type: insert
args:
table: user
objects:
- name: User 1
number: '123456789'
- name: User 2
number: '123456780'
table: author
role: user
permission:
columns:
- name
filter: {}

View File

@ -1,16 +1,9 @@
type: bulk
args:
#Drop relationship first
- type: drop_relationship
args:
relationship: articles
table:
schema: public
name: author
- type: run_sql
args:
sql: |
drop table article;
drop table author;
drop table "user";
DROP TABLE article;
DROP TABLE author;
DROP TABLE "user";
cascade: true

View File

@ -0,0 +1,14 @@
url: /v2/query
status: 200
response:
- id: 1
name: Author 1
- id: 2
name: Author 2
query:
type: select
args:
table: author
columns:
- id
- name

View File

@ -0,0 +1,15 @@
url: /v2/query
headers:
x-hasura-role: user
status: 400
response:
code: access-denied
error: 'restricted access : admin only'
path: $.args
query:
type: select
args:
table: article
columns:
- id
- name

View File

@ -0,0 +1,11 @@
url: /v2/query
status: 200
response:
- name: Author 1
- name: Author 2
query:
type: select
args:
table: author
columns:
- name

View File

@ -0,0 +1,104 @@
type: bulk
args:
#Author table
- type: run_sql
args:
sql: |
create table author(
id serial primary key,
name text unique
);
CREATE TABLE article (
id SERIAL PRIMARY KEY,
title TEXT,
content TEXT,
author_id INTEGER REFERENCES author(id),
is_published BOOLEAN,
published_on TIMESTAMP
);
insert into author (name)
values
('Author 1'),
('Author 2') ;
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
);
CREATE TABLE "user" (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
number BIGINT
);
- type: track_table
args:
schema: public
name: author
#Article table
- type: track_table
args:
schema: public
name: article
#Object relationship
- type: create_object_relationship
args:
table: article
name: author
using:
foreign_key_constraint_on: author_id
#Array relationship
- type: create_array_relationship
args:
table: author
name: articles
using:
foreign_key_constraint_on:
table: article
column: author_id
#Insert values
- type: track_table
args:
schema: public
name: user
- type: insert
args:
table: user
objects:
- name: User 1
number: '123456789'
- name: User 2
number: '123456780'
- type: create_select_permission
args:
table:
schema: public
name: author
role: user
permission:
columns:
- name
filter: {}

View File

@ -0,0 +1,16 @@
type: bulk
args:
#Drop relationship first
- type: drop_relationship
args:
relationship: articles
table:
schema: public
name: author
- type: run_sql
args:
sql: |
drop table article cascade;
drop table author cascade;
drop table "user" cascade;

View File

@ -3,6 +3,20 @@ import re
import json
class TestConfigAPI():
def test_config_api_user_role_error(self, hge_ctx):
admin_secret = hge_ctx.hge_key
auth_hook = hge_ctx.hge_webhook
jwt_conf = hge_ctx.hge_jwt_conf
if jwt_conf is not None:
jwt_conf_dict = json.loads(hge_ctx.hge_jwt_conf)
headers = { 'x-hasura-role': 'user' }
if admin_secret is not None:
headers['x-hasura-admin-secret'] = admin_secret
resp = hge_ctx.http.get(hge_ctx.hge_url + '/v1alpha1/config', headers=headers)
assert resp.status_code == 400, resp
def test_config_api(self, hge_ctx):
admin_secret = hge_ctx.hge_key
@ -11,14 +25,14 @@ class TestConfigAPI():
if jwt_conf is not None:
jwt_conf_dict = json.loads(hge_ctx.hge_jwt_conf)
headers = {}
headers = { 'x-hasura-role': 'admin' }
if admin_secret is not None:
headers['x-hasura-admin-secret'] = admin_secret
resp = hge_ctx.http.get(hge_ctx.hge_url + '/v1alpha1/config', headers=headers)
assert resp.status_code == 200, resp
body = resp.json()
# The tree may be dirty because we're developing tests locally while
# graphql-engine was built previously when tree was clean. If we're

View File

@ -0,0 +1,63 @@
import requests
import pytest
from context import PytestConf
"""
NOTE:
These endpoints are admin-only
"dev/ekg"
"dev/plan_cache"
"dev/subscriptions"
"dev/subscriptions/extended"
This needs RTS to be enabled and mainly used for benchmarking:
(hence not adding any tests for this)
"dev/rts_stats" - has no "admin" role requirements
"""
def get_headers(hge_ctx, role='admin'):
headers = {}
if hge_ctx.hge_key != None:
headers['x-hasura-admin-secret'] = hge_ctx.hge_key
headers['x-hasura-role'] = role
return headers
developer_api_enabled = PytestConf.config.getoption("--test-developer-api-enabled")
@pytest.mark.skipif(not developer_api_enabled,
reason="flag --test-developer-api-enabled is not set. Cannot run tests for metadata disabled")
class TestDevEndpoints:
def test_ekg_endpoint_admin_role(self, hge_ctx):
resp = requests.get(hge_ctx.hge_url + '/dev/ekg', headers=get_headers(hge_ctx))
assert resp.status_code == 200
def test_ekg_endpoint_user_role(self, hge_ctx):
resp = requests.get(hge_ctx.hge_url + '/dev/ekg', headers=get_headers(hge_ctx, 'user'))
assert resp.status_code == 400
def test_plan_cache_endpoint_admin_role(self, hge_ctx):
resp = requests.get(hge_ctx.hge_url + '/dev/plan_cache', headers=get_headers(hge_ctx))
assert resp.status_code == 200
def test_plan_cache_endpoint_user_role(self, hge_ctx):
resp = requests.get(hge_ctx.hge_url + '/dev/plan_cache', headers=get_headers(hge_ctx, 'user'))
assert resp.status_code == 400
def test_subscriptions_endpoint_admin_role(self, hge_ctx):
resp = requests.get(hge_ctx.hge_url + '/dev/subscriptions', headers=get_headers(hge_ctx))
assert resp.status_code == 200
def test_subscriptions_endpoint_user_role(self, hge_ctx):
resp = requests.get(hge_ctx.hge_url + '/dev/subscriptions', headers=get_headers(hge_ctx, 'user'))
assert resp.status_code == 400
def test_subscriptions_extended_endpoint_admin_role(self, hge_ctx):
resp = requests.get(hge_ctx.hge_url + '/dev/subscriptions/extended', headers=get_headers(hge_ctx))
assert resp.status_code == 200
def test_subscriptions_extended_endpoint_user_role(self, hge_ctx):
resp = requests.get(hge_ctx.hge_url + '/dev/subscriptions/extended', headers=get_headers(hge_ctx, 'user'))
assert resp.status_code == 400

View File

@ -20,6 +20,9 @@ class TestCustomEndpoints:
def test_missing_endpoint(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/endpoint_missing.yaml', transport)
def test_endpoint_as_user_err(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/endpoint_as_user_err.yaml', transport)
def test_simple_endpoint(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/endpoint_simple.yaml', transport)

View File

@ -138,6 +138,9 @@ class TestGraphQLQueryBasicCommon:
def test_select_query_author(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/select_query_author.yaml', transport)
def test_select_query_author(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/select_query_author_v1alpha1.yaml', transport)
def test_select_query_author_quoted_col(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/select_query_author_col_quoted.yaml', transport)
@ -544,6 +547,9 @@ class TestGraphqlQueryPermissions:
def test_anonymous_only_published_articles(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/anonymous_can_only_get_published_articles.yaml', transport)
def test_anonymous_only_published_articles_v1alpha1(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/anonymous_can_only_get_published_articles_v1alpha1.yaml', transport)
def test_user_cannot_access_remarks_col(self, hge_ctx, transport):
check_query_f(hge_ctx, self.dir() + '/user_cannot_access_remarks_col.yaml', transport)
@ -1008,20 +1014,40 @@ class TestGraphQLExplain:
def dir(cls):
return 'queries/explain'
def test_simple_query_as_admin(self, hge_ctx, backend):
q = {"query": {"query": "query abc { __typename }", "operationName": "abc"}}
st_code, resp = hge_ctx.v1GraphqlExplain(q)
assert st_code == 200, resp
def test_simple_query_as_user(self, hge_ctx, backend):
q = {"query": {"query": "query abc { __typename }", "operationName": "abc"}}
st_code, resp = hge_ctx.v1GraphqlExplain(q, {"x-hasura-role": "random_user"})
assert st_code == 400, resp
def test_simple_query_as_admin_with_user_role(self, hge_ctx, backend):
self.with_admin_secret(hge_ctx, self.dir() + hge_ctx.backend_suffix('/permissions_query') + ".yaml")
def test_simple_query(self, hge_ctx, backend):
self.with_admin_secret(hge_ctx, self.dir() + hge_ctx.backend_suffix('/simple_query') + ".yaml")
def test_permissions_query(self, hge_ctx, backend):
self.with_admin_secret(hge_ctx, self.dir() + hge_ctx.backend_suffix('/permissions_query') + ".yaml")
def with_admin_secret(self, hge_ctx, f):
def with_admin_secret(self, hge_ctx, f, hdrs=None, req_st=200):
conf = get_conf_f(f)
admin_secret = hge_ctx.hge_key
headers = {}
if admin_secret:
headers['X-Hasura-Admin-Secret'] = hge_ctx.hge_key
if hdrs != None:
headers = hdrs
elif admin_secret and hdrs == None:
headers['X-Hasura-Admin-Secret'] = admin_secret
status_code, resp_json, _ = hge_ctx.anyq(conf['url'], conf['query'], headers)
assert status_code == 200, resp_json
assert status_code == req_st, resp_json
if req_st != 200:
# return early in case we're testing for failures
return
# Comparing only with generated 'sql' since the 'plan' may differ
resp_sql = resp_json[0]['sql']
exp_sql = conf['response'][0]['sql']

View File

@ -25,6 +25,9 @@ class TestMetadata:
def test_clear_metadata(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/clear_metadata.yaml')
def test_clear_metadata_as_user(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/metadata_as_user_err.yaml')
def test_replace_metadata(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/replace_metadata.yaml')

View File

@ -20,7 +20,7 @@ class TestPGDump:
PG_VERSION = os.getenv('PG_VERSION', 'latest')
with open(query_file, 'r') as stream:
q = yaml.safe_load(stream)
headers = {}
headers = q['headers'] or {}
if hge_ctx.hge_key is not None:
headers['x-hasura-admin-secret'] = hge_ctx.hge_key
resp = hge_ctx.http.post(hge_ctx.hge_url + q['url'], json=q['query'], headers=headers)
@ -30,6 +30,18 @@ class TestPGDump:
print(q[resp_pg_version_map[PG_VERSION]])
assert body == q[resp_pg_version_map[PG_VERSION]]
def test_pg_dump_for_public_schema_for_user_role(self, hge_ctx):
query_file = self.dir() + '/pg_dump_public.yaml'
with open(query_file, 'r') as stream:
q = yaml.safe_load(stream)
headers = q['headers'] or {}
if hge_ctx.hge_key is not None:
headers['x-hasura-admin-secret'] = hge_ctx.hge_key
headers['X-Hasura-Role'] = 'user'
resp = hge_ctx.http.post(hge_ctx.hge_url + q['url'], json=q['query'], headers=headers)
body = resp.text
assert resp.status_code == 400, body
@classmethod
def dir(cls):
return "pgdump"

View File

@ -34,9 +34,16 @@ class TestV1General:
@usefixtures('per_class_tests_db_state')
class TestV1SelectBasic:
def test_select_query_author(self, hge_ctx):
def test_select_query_author_with_admin_role(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/select_article.yaml')
# TODO: fix these tests for JWT tests
# def test_select_query_author_with_user_role_success(self, hge_ctx):
# check_query_f(hge_ctx, self.dir() + '/select_article_role_success.yaml')
# def test_select_query_author_with_user_role_failure(self, hge_ctx):
# check_query_f(hge_ctx, self.dir() + '/select_article_role_error.yaml')
def test_nested_select_article_author(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/nested_select_query_article_author.yaml')
@ -498,6 +505,11 @@ class TestRunSQL:
check_query_f(hge_ctx, self.dir() + '/sql_select_query.yaml')
hge_ctx.may_skip_test_teardown = True
# TODO: create a v2 query tests module
def test_select_query_v2(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/sql_select_query_v2.yaml')
hge_ctx.may_skip_test_teardown = True
def test_select_query_read_only(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/sql_select_query_read_only.yaml')
hge_ctx.may_skip_test_teardown = True
@ -512,6 +524,10 @@ class TestRunSQL:
def test_sql_query_as_user_error(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/sql_query_as_user_error.yaml')
# TODO: create a v2 query tests module
def test_sql_query_as_user_error_v2(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/sql_query_as_user_error_v2.yaml')
def test_sql_rename_table(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/sql_rename_table.yaml')

View File

@ -0,0 +1,25 @@
from validate import check_query_f
import pytest
usefixtures = pytest.mark.usefixtures
# use_mutation_fixtures = usefixtures(
# 'per_class_db_schema_for_mutation_tests',
# 'per_method_db_data_for_mutation_tests'
# )
@usefixtures('per_class_tests_db_state')
class TestV2SelectBasic: # Basic RQL Tests on v2/query
@classmethod
def dir(cls):
return 'queries/v2/basic'
def test_select_query_author(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/select_article.yaml')
def test_select_query_author_with_user_role_success(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/select_article_role_success.yaml')
# TODO: Fix this test for JWT
# def test_select_query_author_with_user_role_failure(self, hge_ctx):
# check_query_f(hge_ctx, self.dir() + '/select_article_role_error.yaml')