From 827e84ee18169179181b40a3b5980fc01a90202a Mon Sep 17 00:00:00 2001 From: Dan Debrunner Date: Fri, 27 Jun 2025 08:56:10 -0400 Subject: [PATCH 1/3] chore: add visibility example --- protection/README.md | 1 + protection/visibility/README.md | 168 ++++++++++++++++++++++ protection/visibility/customer.graphql | 21 +++ protection/visibility/index.graphql | 7 + protection/visibility/stepzen.config.json | 3 + 5 files changed, 200 insertions(+) create mode 100644 protection/visibility/README.md create mode 100644 protection/visibility/customer.graphql create mode 100644 protection/visibility/index.graphql create mode 100644 protection/visibility/stepzen.config.json diff --git a/protection/README.md b/protection/README.md index 9bede8b..34bae88 100644 --- a/protection/README.md +++ b/protection/README.md @@ -7,3 +7,4 @@ For more information on protecting your API, [see our documentation](https://www - [makeAllPublic](makeAllPublic) shows how you can easily make all `Query` fields public, thus resulting in a public endpoint. - [makeSomePublic](makeSomePublic) shows how you can make fields public, and some private (which can still be accessed using your `admin` or `service` keys). - [simpleABACSample](simpleABACSample) shows how to control access to fields using JWT claims. +- [visbility](visibility) shows use of schema controlled visibility of fields. diff --git a/protection/visibility/README.md b/protection/visibility/README.md new file mode 100644 index 0000000..3e61479 --- /dev/null +++ b/protection/visibility/README.md @@ -0,0 +1,168 @@ +# Visibility + +## Overview + +Visibility patterns allows fine-grained control over fields that are exposed by a schema, including introspection and handling requests. + +With directives `@materializer`, `@sequence`, `@supplies` and `@inject` fields can be resolved through resolution of other fields. + +For example, here the field `Customer.orders` is resolved by the resolution of `Query.orders`, using `@materializer`. + +```graphql +type Query { + customer(email: String!): Customer @rest(endpoint: "...") + + orders(customer_id: ID!): [Order] @dbquery(type: "postgresql") +} + +type Customer { + id: ID! + name: String + email: String + orders: [Order] + @materializer( + query: "orders" + arguments: { name: "customer_id", field: "id" } + ) +} +``` + +(details for `@rest`, `@dbquery` omitted for clarity) + +In this case, we assume that the schema developer only wants to have clients obtain customer information through `Query.customer`, +including their orders, that is they want to only have clients use the _graph_ defined by the schema (customers have orders). + +This means they do not want clients to make a request such as `{orders(customer_id:1) {date cost}}`, instead only +obtain orders through `{customer(email:"alice@example.com") {name orders {date cost}}}`. + +While this can be achieved by field access policies, visibility provides a scoping mechanism for fields within the schema definition (`*.graphql` files) itself. + +And fields hidden by visibility are effectively not part of the external GraphQL schema and thus cannot be selected by a request +or inspected using GraphQL introspection. + +Visibiilty is applied before field access policies, as field access policies apply to the external schema of an endpoint. + +> [!WARNING] +> Visibility patterns are not applied for requests make with an admin key, thus the full schema definition from the `*.graphql` files is exposed. This is to aid debugging of schemas. + +## Visibility patterns + +Visibility is controlled through the directive argument `@sdl(visibility:)`. + +The visibility patterns apply only to the schema elements that are included through `@sdl(files:)`. + +For our example above we assume the schema is in `customer.graphl`, thus our `index.graphql` would look like: + +```graphql +schema + @sdl( + files: "customer.graphql" + visibility: { expose: true, types: "Query", fields: "customer" } + ) { + query: Query +} +``` + +Fields that match the pattern are defined using regular expressions in `types` and `fields`, that match type names and field names. + +Defaults match the style of field access policies in that: + +- Root operation type fields (`Query`, `Mutation`, `Subscription`) are not exposed by default. +- All other fields in object and interface types are exposed by default. + +Thus in this simple example all fields in `Query` are not exposed with the exception of `Query.customer`. + +The external schema will only include `Query.customer` and and schema elements reachable from that field. + +> [!NOTE] +> Any fields defined in this `index.graphql` are **not** subject to the visibility patterns, as patterns only apply to the schema elements that are included through files listed in `@sdl(files:)`. + +## Consistent field naming + +Visibility patterns encourage a consistent naming policy for a GraphQL schema. +For example using the prefix `_` for any "internal" field not to be exposed, can be enforced using a visibility pattern such as: + +```graphql +schema + @sdl( + files: "customer.graphql" + visibility: [ + { expose: true, types: "Query", fields: "customer" } + { expose: false, types: ".*", fields: "_.*" } # Any type, any field whose name starts with _ + ] + ) { + query: Query +} +``` + +> [!TIP] +> Double underscore `__` as a prefix is reserved for GraphQL introspection and is not allowed. + +## Try it out + +Deploy the schema in this folder and then introspect the schema. + +This lists the fields in `Query` + +```graphql +query { + __schema { + queryType { + fields { + name + } + } + } +} +``` + +The response is, showing `Query.orders` is not visible: + +```json +{ + "data": { + "__schema": { + "description": "", + "queryType": { + "fields": [ + { + "name": "customer" + } + ] + } + } + } +} +``` + +You can verify the `Query.orders` cannot be selected: + +```graphql +query { + orders(customer_id: 1) { + date + when + } +} +``` + +results in: + +```json +{ + "errors": [ + { + "message": "Cannot query field \"orders\" on type \"Query\".", + "locations": [ + { + "line": 1, + "column": 9 + } + ] + } + ] +} +``` + +> [!NOTE] +> If you see `Query.orders` then check if you are using the admin key in your request. diff --git a/protection/visibility/customer.graphql b/protection/visibility/customer.graphql new file mode 100644 index 0000000..99776b6 --- /dev/null +++ b/protection/visibility/customer.graphql @@ -0,0 +1,21 @@ +type Query { + customer(email: String!): Customer + + orders(customer_id: ID!): [Order] +} + +type Customer @mock { + id: ID! + name: String! @mockfn(name: "LastName") + email: String! + orders: [Order] + @materializer( + query: "orders" + arguments: { name: "customer_id", field: "id" } + ) +} + +type Order @mock { + date: Date! @mockfn(name: "PastDate", values: 5) + cost: Float! +} diff --git a/protection/visibility/index.graphql b/protection/visibility/index.graphql new file mode 100644 index 0000000..ee0422c --- /dev/null +++ b/protection/visibility/index.graphql @@ -0,0 +1,7 @@ +schema + @sdl( + files: "customer.graphql" + visibility: { expose: true, types: "Query", fields: "customer" } + ) { + query: Query +} diff --git a/protection/visibility/stepzen.config.json b/protection/visibility/stepzen.config.json new file mode 100644 index 0000000..abe76e9 --- /dev/null +++ b/protection/visibility/stepzen.config.json @@ -0,0 +1,3 @@ +{ + "endpoint": "api/miscellaneous" +} \ No newline at end of file From fa15bd9e9e2454ef24881deb285977e44827bbb8 Mon Sep 17 00:00:00 2001 From: Dan Debrunner Date: Fri, 27 Jun 2025 09:07:01 -0400 Subject: [PATCH 2/3] test: add tests --- protection/visibility/customer.graphql | 2 +- protection/visibility/tests/Test.js | 45 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 protection/visibility/tests/Test.js diff --git a/protection/visibility/customer.graphql b/protection/visibility/customer.graphql index 99776b6..2e47434 100644 --- a/protection/visibility/customer.graphql +++ b/protection/visibility/customer.graphql @@ -17,5 +17,5 @@ type Customer @mock { type Order @mock { date: Date! @mockfn(name: "PastDate", values: 5) - cost: Float! + cost: Int! @mockfn(name: "NumberRange", values: [1, 500]) } diff --git a/protection/visibility/tests/Test.js b/protection/visibility/tests/Test.js new file mode 100644 index 0000000..0f6df19 --- /dev/null +++ b/protection/visibility/tests/Test.js @@ -0,0 +1,45 @@ +const { + deployAndRun, + stepzen, + getTestDescription, +} = require("../../../tests/gqltest.js"); + +testDescription = getTestDescription("snippets", __dirname); + +describe(testDescription, function () { + const tests = [ + { + label: "customer", + query: + '{customer(email:"alice@example.com") {id name email orders {cost}}}', + expected: { + customer: { + id: "464979", + name: "Lesch", + email: "alice@example.com", + orders: [ + { + cost: 100, + }, + ], + }, + }, + }, + { + label: "query-fields", + query: "{__schema {queryType {fields {name}}}}", + expected: { + __schema: { + queryType: { + fields: [ + { + name: "customer", + }, + ], + }, + }, + }, + }, + ]; + return deployAndRun(__dirname, tests, stepzen.admin); +}); From d3ac08a26373b8f6cfa1b2647245f3f7bb209123 Mon Sep 17 00:00:00 2001 From: Dan Debrunner Date: Fri, 27 Jun 2025 09:09:05 -0400 Subject: [PATCH 3/3] test: use api key --- protection/visibility/tests/Test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protection/visibility/tests/Test.js b/protection/visibility/tests/Test.js index 0f6df19..68808e7 100644 --- a/protection/visibility/tests/Test.js +++ b/protection/visibility/tests/Test.js @@ -41,5 +41,5 @@ describe(testDescription, function () { }, }, ]; - return deployAndRun(__dirname, tests, stepzen.admin); + return deployAndRun(__dirname, tests, stepzen.regular); });