From b7bc5277a853eb2968ce2a7bc86e8d017903bc23 Mon Sep 17 00:00:00 2001 From: Dan Debrunner Date: Wed, 19 Jun 2024 18:07:01 -0400 Subject: [PATCH 01/10] jwt-claims as arguments example --- protection/jwt-claims/README.md | 60 +++++++++++++++++++++++ protection/jwt-claims/api.graphql | 27 ++++++++++ protection/jwt-claims/config.yaml | 21 ++++++++ protection/jwt-claims/index.graphql | 14 ++++++ protection/jwt-claims/operations.graphql | 6 +++ protection/jwt-claims/stepzen.config.json | 3 ++ 6 files changed, 131 insertions(+) create mode 100644 protection/jwt-claims/README.md create mode 100644 protection/jwt-claims/api.graphql create mode 100644 protection/jwt-claims/config.yaml create mode 100644 protection/jwt-claims/index.graphql create mode 100644 protection/jwt-claims/operations.graphql create mode 100644 protection/jwt-claims/stepzen.config.json diff --git a/protection/jwt-claims/README.md b/protection/jwt-claims/README.md new file mode 100644 index 0000000..91107e2 --- /dev/null +++ b/protection/jwt-claims/README.md @@ -0,0 +1,60 @@ +# Pulling field arguments from JWT claims + +This snippet shows how JWT claims can be used for field arguments. + +This examples demonstrates an number of StepZen capabilities: + +- Authorization using JWT +- Field access rules +- Field visibility rules +- `@value` directive with access to JWT claims +- Reshaping + +## Restricting access through JWT claims. + +This example shows how a JWT claim may be used as a field argument, in this case the JWT subject claim +is used as a customer's identifier. Thus the customer can only view their own information even though +the backend database includes all customers. + +The field `Query.customer` provides an identifier lookup to all customers. This field is restricted +from been executed by an authenticated user by field visibility and access rules. + +Instead a field `Query.me` is exposed with no field arguments that invokes `Query.customer` +with the customer identifier pulled from the `sub` claim in the request's JWT. + +This is implemented by an intermediate field (`Query._myid`) that is annotated with `@value` using an ECMAScript. +This script has access to field arguments of its annotated field (in this case none) and JWT claims through `$jwt`. +Thus it returns the `sub` claim which is then automatically mapped as a scalar value to the sole argument of +the next step in the sequence (`Query.customer(id:)`). + +Note these concepts could be combined with field access through RBAC rules (see XXXX) +so that `Query.customer` could be exposed, but only customer service reps could call it +with an arbitary identifier. + +## Try it out! + +Deploy the schema from `protection/jwt-claims` relative to the repository's root directory: + +``` +stepzen deploy +``` + +Run the [sample operations](operations.graphql): + +JWT with `sub: 5`. + +JWT: https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1In0.LE_mbGsS2FbxF41r4wOYKhWdBoYhnIk0-6d6U7ibF-A +Secret Key: development-only + +``` +stepzen request -f operations.graphql --operation-name=Customer --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1In0.LE_mbGsS2FbxF41r4wOYKhWdBoYhnIk0-6d6U7ibF-A" +``` + +JWT with `sub: 9`. + +JWT: https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1In0.LE_mbGsS2FbxF41r4wOYKhWdBoYhnIk0-6d6U7ibF-A +Secret Key: development-only + +``` +stepzen request -f operations.graphql --operation-name=Customer --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5In0.liX79YjQ1EIqdVSLqvKoVJxoj63OkBANwZLsZcdLzDM" +``` diff --git a/protection/jwt-claims/api.graphql b/protection/jwt-claims/api.graphql new file mode 100644 index 0000000..d0be2fb --- /dev/null +++ b/protection/jwt-claims/api.graphql @@ -0,0 +1,27 @@ +type Customer { + id: ID! + name: String + email: String +} + +type Query { + customer(id: ID!): Customer + @dbquery( + type: "postgresql" + table: "customer" + schema: "public" + configuration: "postgresql_config" + ) + + me: Customer @sequence(steps: [{ query: "_myid" }, { query: "customer" }]) + + """ + Extract the customer's identifier from a claim in a JWT. + """ + _myid: ID! @value(script: { src: "$jwt.sub" }) + + """ + Extract the customer's identifier from a claim in a JWT using JSONATA. + """ + _myid_jsonata: ID! @value(script: { src: "`$jwt`.sub", language: JSONATA }) +} diff --git a/protection/jwt-claims/config.yaml b/protection/jwt-claims/config.yaml new file mode 100644 index 0000000..c7ab6ec --- /dev/null +++ b/protection/jwt-claims/config.yaml @@ -0,0 +1,21 @@ +deployment: + identity: + keys: + - algorithm: HS256 + key: development-only +access: + policies: + - type: Query + policyDefault: + condition: false + rules: + - name: "jwt-control" + fields: [me] + condition: "?$jwt" + - name: "introspection" + fields: [__schema, __type] + condition: true +configurationset: + - configuration: + name: postgresql_config + uri: postgresql://postgresql.introspection.stepzen.net/introspection?user=testUserIntrospection&password=HurricaneStartingSample1934 diff --git a/protection/jwt-claims/index.graphql b/protection/jwt-claims/index.graphql new file mode 100644 index 0000000..b7ce5a6 --- /dev/null +++ b/protection/jwt-claims/index.graphql @@ -0,0 +1,14 @@ +schema + @sdl( + files: ["api.graphql"] + # visibilty controls how fields included through files in this directive + # are visible outside the scope of this directive to GraphQL introspection + # and field references through @materializer etc. + # + # types and fields are regular expressions that match type and field names. + # Like field access rules if aat least one visibility pattern is present then by default + # root operation type (Query, Mutation, Subscription) fields are not exposed. + visibility: [{ expose: true, types: "Query", fields: "me" }] + ) { + query: Query +} diff --git a/protection/jwt-claims/operations.graphql b/protection/jwt-claims/operations.graphql new file mode 100644 index 0000000..def2613 --- /dev/null +++ b/protection/jwt-claims/operations.graphql @@ -0,0 +1,6 @@ +query Customer { + me { + id + name + } +} diff --git a/protection/jwt-claims/stepzen.config.json b/protection/jwt-claims/stepzen.config.json new file mode 100644 index 0000000..af1c0ea --- /dev/null +++ b/protection/jwt-claims/stepzen.config.json @@ -0,0 +1,3 @@ +{ + "endpoint": "api/miscellaneous" +} From cb2070c340e53233f36bb83225a5c072af0d46ad Mon Sep 17 00:00:00 2001 From: Dan Debrunner Date: Wed, 19 Jun 2024 18:13:07 -0400 Subject: [PATCH 02/10] cleanup --- protection/jwt-claims/README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/protection/jwt-claims/README.md b/protection/jwt-claims/README.md index 91107e2..c7d2a4a 100644 --- a/protection/jwt-claims/README.md +++ b/protection/jwt-claims/README.md @@ -2,12 +2,13 @@ This snippet shows how JWT claims can be used for field arguments. -This examples demonstrates an number of StepZen capabilities: +This examples demonstrates a number of StepZen capabilities: - Authorization using JWT - Field access rules - Field visibility rules - `@value` directive with access to JWT claims + - https://www.ibm.com/docs/en/api-connect/ace/saas?topic=directives-directive-value - Reshaping ## Restricting access through JWT claims. @@ -17,7 +18,7 @@ is used as a customer's identifier. Thus the customer can only view their own in the backend database includes all customers. The field `Query.customer` provides an identifier lookup to all customers. This field is restricted -from been executed by an authenticated user by field visibility and access rules. +from being executed by field visibility and access rules. Instead a field `Query.me` is exposed with no field arguments that invokes `Query.customer` with the customer identifier pulled from the `sub` claim in the request's JWT. @@ -27,9 +28,12 @@ This script has access to field arguments of its annotated field (in this case n Thus it returns the `sub` claim which is then automatically mapped as a scalar value to the sole argument of the next step in the sequence (`Query.customer(id:)`). -Note these concepts could be combined with field access through RBAC rules (see XXXX) +An alternate version of `Query._myid` exists `Query._my_id_jsonata` showing that scripts can be implemented in JSONata. +The default langauge is ECMAScript. + +Note these concepts could be combined with field access through ABAC rules (see `protection/simpleABACSample`) so that `Query.customer` could be exposed, but only customer service reps could call it -with an arbitary identifier. +with any customer identifier. ## Try it out! @@ -44,6 +48,7 @@ Run the [sample operations](operations.graphql): JWT with `sub: 5`. JWT: https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1In0.LE_mbGsS2FbxF41r4wOYKhWdBoYhnIk0-6d6U7ibF-A + Secret Key: development-only ``` @@ -53,6 +58,7 @@ stepzen request -f operations.graphql --operation-name=Customer --header "Author JWT with `sub: 9`. JWT: https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1In0.LE_mbGsS2FbxF41r4wOYKhWdBoYhnIk0-6d6U7ibF-A + Secret Key: development-only ``` From 819d7479dadbb758495fd23198f68b355c92048b Mon Sep 17 00:00:00 2001 From: Dan Debrunner Date: Wed, 19 Jun 2024 18:16:06 -0400 Subject: [PATCH 03/10] cleanup --- protection/jwt-claims/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protection/jwt-claims/README.md b/protection/jwt-claims/README.md index c7d2a4a..f1a5029 100644 --- a/protection/jwt-claims/README.md +++ b/protection/jwt-claims/README.md @@ -23,7 +23,7 @@ from being executed by field visibility and access rules. Instead a field `Query.me` is exposed with no field arguments that invokes `Query.customer` with the customer identifier pulled from the `sub` claim in the request's JWT. -This is implemented by an intermediate field (`Query._myid`) that is annotated with `@value` using an ECMAScript. +This is implemented using `@sequence` with the first step an intermediate field (`Query._myid`) that is annotated with `@value` using an ECMAScript. This script has access to field arguments of its annotated field (in this case none) and JWT claims through `$jwt`. Thus it returns the `sub` claim which is then automatically mapped as a scalar value to the sole argument of the next step in the sequence (`Query.customer(id:)`). From 4202fda0117a27e83b21a559fea329545a337a12 Mon Sep 17 00:00:00 2001 From: Dan Debrunner Date: Wed, 19 Jun 2024 18:20:48 -0400 Subject: [PATCH 04/10] cleanup --- protection/jwt-claims/README.md | 2 +- protection/jwt-claims/api.graphql | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/protection/jwt-claims/README.md b/protection/jwt-claims/README.md index f1a5029..dab7022 100644 --- a/protection/jwt-claims/README.md +++ b/protection/jwt-claims/README.md @@ -23,7 +23,7 @@ from being executed by field visibility and access rules. Instead a field `Query.me` is exposed with no field arguments that invokes `Query.customer` with the customer identifier pulled from the `sub` claim in the request's JWT. -This is implemented using `@sequence` with the first step an intermediate field (`Query._myid`) that is annotated with `@value` using an ECMAScript. +This is implemented using `@sequence` with the first step an intermediate field (`Query._myid`) that is annotated with `@value` using ECMAScript. This script has access to field arguments of its annotated field (in this case none) and JWT claims through `$jwt`. Thus it returns the `sub` claim which is then automatically mapped as a scalar value to the sole argument of the next step in the sequence (`Query.customer(id:)`). diff --git a/protection/jwt-claims/api.graphql b/protection/jwt-claims/api.graphql index d0be2fb..a409a7f 100644 --- a/protection/jwt-claims/api.graphql +++ b/protection/jwt-claims/api.graphql @@ -5,6 +5,9 @@ type Customer { } type Query { + """ + Fetch customer by identifier. + """ customer(id: ID!): Customer @dbquery( type: "postgresql" @@ -13,6 +16,10 @@ type Query { configuration: "postgresql_config" ) + """ + Fetch my customer information. + Customer identifier is pulled from the JWT subject claim. + """ me: Customer @sequence(steps: [{ query: "_myid" }, { query: "customer" }]) """ From af380b22d5ab0f1c9a496748a25820829739e885 Mon Sep 17 00:00:00 2001 From: Dan Debrunner Date: Wed, 19 Jun 2024 18:21:16 -0400 Subject: [PATCH 05/10] cleanup --- protection/jwt-claims/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protection/jwt-claims/README.md b/protection/jwt-claims/README.md index dab7022..ed064ba 100644 --- a/protection/jwt-claims/README.md +++ b/protection/jwt-claims/README.md @@ -6,7 +6,7 @@ This examples demonstrates a number of StepZen capabilities: - Authorization using JWT - Field access rules -- Field visibility rules +- Field visibility patterns - `@value` directive with access to JWT claims - https://www.ibm.com/docs/en/api-connect/ace/saas?topic=directives-directive-value - Reshaping From 457e8f06019960d66c955aa5c2d110ea637da9d6 Mon Sep 17 00:00:00 2001 From: Dan Debrunner Date: Tue, 2 Jul 2024 17:18:02 -0400 Subject: [PATCH 06/10] wip - JWT claims with explicit SQL predicate --- protection/jwt-claims-dbquery/README.md | 29 ++++++ protection/jwt-claims-dbquery/config.yaml | 21 ++++ protection/jwt-claims-dbquery/index.graphql | 14 +++ .../jwt-claims-dbquery/operations.graphql | 8 ++ protection/jwt-claims-dbquery/paging.graphql | 95 +++++++++++++++++++ .../jwt-claims-dbquery/stepzen.config.json | 3 + 6 files changed, 170 insertions(+) create mode 100644 protection/jwt-claims-dbquery/README.md create mode 100644 protection/jwt-claims-dbquery/config.yaml create mode 100644 protection/jwt-claims-dbquery/index.graphql create mode 100644 protection/jwt-claims-dbquery/operations.graphql create mode 100644 protection/jwt-claims-dbquery/paging.graphql create mode 100644 protection/jwt-claims-dbquery/stepzen.config.json diff --git a/protection/jwt-claims-dbquery/README.md b/protection/jwt-claims-dbquery/README.md new file mode 100644 index 0000000..5c6c760 --- /dev/null +++ b/protection/jwt-claims-dbquery/README.md @@ -0,0 +1,29 @@ +# Pulling field arguments from JWT claims + +Run the [sample operations](operations.graphql): + +JWT with `regions: IN`. +``` +stepzen request -f operations.graphql --operation-name=Customers \ + --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJJTiJdfQ.hDi3-qaIOSFKzlFvaXwSh0trXC3vjiOehSKE0OxgOdE" +``` + + +JWT with `regions: IN, UK`. +``` +stepzen request -f operations.graphql --operation-name=Customers \ + --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJJTiJdfQ.hDi3-qaIOSFKzlFvaXwSh0trXC3vjiOehSKE0OxgOdE" +``` + +JWT with `regions: US, UK`. +``` +stepzen request -f operations.graphql --operation-name=Customers \ + --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJVUyIsIlVLIl19.pf0-A6TN_hT-ldCvsZyqYGv4Twjm9s6wO1aatCjK9Aw" +``` + +JWT with `regions: US, UK` and user supplied filter +``` +stepzen request -f operations.graphql --operation-name=Customers \ + --var f='{"city": {"eq":"London"}}' \ + --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJVUyIsIlVLIl19.pf0-A6TN_hT-ldCvsZyqYGv4Twjm9s6wO1aatCjK9Aw" +``` diff --git a/protection/jwt-claims-dbquery/config.yaml b/protection/jwt-claims-dbquery/config.yaml new file mode 100644 index 0000000..0d28abe --- /dev/null +++ b/protection/jwt-claims-dbquery/config.yaml @@ -0,0 +1,21 @@ +deployment: + identity: + keys: + - algorithm: HS256 + key: development-only +access: + policies: + - type: Query + policyDefault: + condition: false + rules: + - name: "jwt-control" + fields: [customers] + condition: "?$jwt" + - name: "introspection" + fields: [__schema, __type] + condition: true +configurationset: + - configuration: + name: postgresql_config + uri: postgresql://postgresql.introspection.stepzen.net/introspection?user=testUserIntrospection&password=HurricaneStartingSample1934 diff --git a/protection/jwt-claims-dbquery/index.graphql b/protection/jwt-claims-dbquery/index.graphql new file mode 100644 index 0000000..538bc52 --- /dev/null +++ b/protection/jwt-claims-dbquery/index.graphql @@ -0,0 +1,14 @@ +schema + @sdl( + files: ["paging.graphql"] + # visibilty controls how fields included through files in this directive + # are visible outside the scope of this directive to GraphQL introspection + # and field references through @materializer etc. + # + # types and fields are regular expressions that match type and field names. + # Like field access rules if aat least one visibility pattern is present then by default + # root operation type (Query, Mutation, Subscription) fields are not exposed. + visibility: [{ expose: true, types: "Query", fields: ".*" }] + ) { + query: Query +} diff --git a/protection/jwt-claims-dbquery/operations.graphql b/protection/jwt-claims-dbquery/operations.graphql new file mode 100644 index 0000000..ede5e35 --- /dev/null +++ b/protection/jwt-claims-dbquery/operations.graphql @@ -0,0 +1,8 @@ +query Customers($f:CustomerFilter) { + customers(filter:$f) { + id + name + city + region + } +} diff --git a/protection/jwt-claims-dbquery/paging.graphql b/protection/jwt-claims-dbquery/paging.graphql new file mode 100644 index 0000000..b2165b0 --- /dev/null +++ b/protection/jwt-claims-dbquery/paging.graphql @@ -0,0 +1,95 @@ +type Customer { + id: ID! + name: String + email: String + street: String + city: String + region: String +} + +""" +`CustomerConnection` is the connection type for `Customer` pagination. +In StepZen, a field returning a connection type must have arguments +`first` (number of nodes to fetch) and `after` (opaque cursor indicating +the starting point). +""" +type CustomerConnection { + edges: [CustomerEdge] + pageInfo: PageInfo! +} + +""" +`CustomerEdge` provides access to the node and its cursor. +""" +type CustomerEdge { + node: Customer + cursor: String +} + +input StringFilter { + eq: String + ne: String +} + +input CustomerFilter { + name: StringFilter + email: StringFilter + city: StringFilter +} + +extend type Query { + # customers is the exposed field that limits the caller to regions + # based upon the regions claim in the request's JWT. + customers( + first: Int! = 10 + filter: CustomerFilter + ): [Customer] + @sequence( + steps: [ + { query: "_regions" } + { + query: "_customers_flatten" + arguments: [ + { name: "first", argument: "first" } + { name: "filter", argument: "filter" } + ] + } + ] + ) + + # extracts the regions visible to the request from the JWT. + _regions: [String]! @value(script: { src: "$jwt.regions" }) + + # this flattens the customer connection pagination structure + # into a simple list of Customer objects. + # This is needed as @sequence is not supported for connection types. + _customers_flatten( + first: Int! = 10 + filter: CustomerFilter + regions: [String]! + ): [Customer] @materializer(query: "_customers { edges { node }}") + + # Standard paginated field for a customers table in a database. + # Additional regions argument that is used to limit customer + # visibility based upon the 'regions' claim in a JWT. + # The regions allows a list of regions and uses SQL ANY to match rows. + _customers( + first: Int! = 10 + after: String! = "" + filter: CustomerFilter # regions: [String]! + regions: [String]! + ): CustomerConnection + @dbquery( + type: "postgresql" + schema: "public" + query: """ + SELECT C.id, C.name, C.email, A.street, A.city, A.countryregion AS region + FROM customer C, address A, customeraddress CA + WHERE + CA.customerid = C.id AND + CA.addressid = A.id AND + A.countryregion = ANY(CAST($1 AS VARCHAR ARRAY)) + """ + configuration: "postgresql_config" + ) +} diff --git a/protection/jwt-claims-dbquery/stepzen.config.json b/protection/jwt-claims-dbquery/stepzen.config.json new file mode 100644 index 0000000..af1c0ea --- /dev/null +++ b/protection/jwt-claims-dbquery/stepzen.config.json @@ -0,0 +1,3 @@ +{ + "endpoint": "api/miscellaneous" +} From a397d3a6e8d1133a8eb795b93c911db1e26ebdc5 Mon Sep 17 00:00:00 2001 From: Dan Debrunner Date: Tue, 2 Jul 2024 17:19:39 -0400 Subject: [PATCH 07/10] chore: quick overview --- protection/jwt-claims-dbquery/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/protection/jwt-claims-dbquery/README.md b/protection/jwt-claims-dbquery/README.md index 5c6c760..463601c 100644 --- a/protection/jwt-claims-dbquery/README.md +++ b/protection/jwt-claims-dbquery/README.md @@ -1,5 +1,10 @@ # Pulling field arguments from JWT claims +Uses a SQL predicate to limit customer rows returned from a database +to those matching the regions defined in a JWT claim. + +# Try it Out + Run the [sample operations](operations.graphql): JWT with `regions: IN`. From 42cc9359adc21104c04afe4fa5e60e9bb1e114ec Mon Sep 17 00:00:00 2001 From: Dan Debrunner Date: Tue, 2 Jul 2024 17:21:56 -0400 Subject: [PATCH 08/10] remove dups --- protection/jwt-claims/README.md | 66 ----------------------- protection/jwt-claims/api.graphql | 34 ------------ protection/jwt-claims/config.yaml | 21 -------- protection/jwt-claims/index.graphql | 14 ----- protection/jwt-claims/operations.graphql | 6 --- protection/jwt-claims/stepzen.config.json | 3 -- 6 files changed, 144 deletions(-) delete mode 100644 protection/jwt-claims/README.md delete mode 100644 protection/jwt-claims/api.graphql delete mode 100644 protection/jwt-claims/config.yaml delete mode 100644 protection/jwt-claims/index.graphql delete mode 100644 protection/jwt-claims/operations.graphql delete mode 100644 protection/jwt-claims/stepzen.config.json diff --git a/protection/jwt-claims/README.md b/protection/jwt-claims/README.md deleted file mode 100644 index ed064ba..0000000 --- a/protection/jwt-claims/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Pulling field arguments from JWT claims - -This snippet shows how JWT claims can be used for field arguments. - -This examples demonstrates a number of StepZen capabilities: - -- Authorization using JWT -- Field access rules -- Field visibility patterns -- `@value` directive with access to JWT claims - - https://www.ibm.com/docs/en/api-connect/ace/saas?topic=directives-directive-value -- Reshaping - -## Restricting access through JWT claims. - -This example shows how a JWT claim may be used as a field argument, in this case the JWT subject claim -is used as a customer's identifier. Thus the customer can only view their own information even though -the backend database includes all customers. - -The field `Query.customer` provides an identifier lookup to all customers. This field is restricted -from being executed by field visibility and access rules. - -Instead a field `Query.me` is exposed with no field arguments that invokes `Query.customer` -with the customer identifier pulled from the `sub` claim in the request's JWT. - -This is implemented using `@sequence` with the first step an intermediate field (`Query._myid`) that is annotated with `@value` using ECMAScript. -This script has access to field arguments of its annotated field (in this case none) and JWT claims through `$jwt`. -Thus it returns the `sub` claim which is then automatically mapped as a scalar value to the sole argument of -the next step in the sequence (`Query.customer(id:)`). - -An alternate version of `Query._myid` exists `Query._my_id_jsonata` showing that scripts can be implemented in JSONata. -The default langauge is ECMAScript. - -Note these concepts could be combined with field access through ABAC rules (see `protection/simpleABACSample`) -so that `Query.customer` could be exposed, but only customer service reps could call it -with any customer identifier. - -## Try it out! - -Deploy the schema from `protection/jwt-claims` relative to the repository's root directory: - -``` -stepzen deploy -``` - -Run the [sample operations](operations.graphql): - -JWT with `sub: 5`. - -JWT: https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1In0.LE_mbGsS2FbxF41r4wOYKhWdBoYhnIk0-6d6U7ibF-A - -Secret Key: development-only - -``` -stepzen request -f operations.graphql --operation-name=Customer --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1In0.LE_mbGsS2FbxF41r4wOYKhWdBoYhnIk0-6d6U7ibF-A" -``` - -JWT with `sub: 9`. - -JWT: https://jwt.io/#debugger-io?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1In0.LE_mbGsS2FbxF41r4wOYKhWdBoYhnIk0-6d6U7ibF-A - -Secret Key: development-only - -``` -stepzen request -f operations.graphql --operation-name=Customer --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5In0.liX79YjQ1EIqdVSLqvKoVJxoj63OkBANwZLsZcdLzDM" -``` diff --git a/protection/jwt-claims/api.graphql b/protection/jwt-claims/api.graphql deleted file mode 100644 index a409a7f..0000000 --- a/protection/jwt-claims/api.graphql +++ /dev/null @@ -1,34 +0,0 @@ -type Customer { - id: ID! - name: String - email: String -} - -type Query { - """ - Fetch customer by identifier. - """ - customer(id: ID!): Customer - @dbquery( - type: "postgresql" - table: "customer" - schema: "public" - configuration: "postgresql_config" - ) - - """ - Fetch my customer information. - Customer identifier is pulled from the JWT subject claim. - """ - me: Customer @sequence(steps: [{ query: "_myid" }, { query: "customer" }]) - - """ - Extract the customer's identifier from a claim in a JWT. - """ - _myid: ID! @value(script: { src: "$jwt.sub" }) - - """ - Extract the customer's identifier from a claim in a JWT using JSONATA. - """ - _myid_jsonata: ID! @value(script: { src: "`$jwt`.sub", language: JSONATA }) -} diff --git a/protection/jwt-claims/config.yaml b/protection/jwt-claims/config.yaml deleted file mode 100644 index c7ab6ec..0000000 --- a/protection/jwt-claims/config.yaml +++ /dev/null @@ -1,21 +0,0 @@ -deployment: - identity: - keys: - - algorithm: HS256 - key: development-only -access: - policies: - - type: Query - policyDefault: - condition: false - rules: - - name: "jwt-control" - fields: [me] - condition: "?$jwt" - - name: "introspection" - fields: [__schema, __type] - condition: true -configurationset: - - configuration: - name: postgresql_config - uri: postgresql://postgresql.introspection.stepzen.net/introspection?user=testUserIntrospection&password=HurricaneStartingSample1934 diff --git a/protection/jwt-claims/index.graphql b/protection/jwt-claims/index.graphql deleted file mode 100644 index b7ce5a6..0000000 --- a/protection/jwt-claims/index.graphql +++ /dev/null @@ -1,14 +0,0 @@ -schema - @sdl( - files: ["api.graphql"] - # visibilty controls how fields included through files in this directive - # are visible outside the scope of this directive to GraphQL introspection - # and field references through @materializer etc. - # - # types and fields are regular expressions that match type and field names. - # Like field access rules if aat least one visibility pattern is present then by default - # root operation type (Query, Mutation, Subscription) fields are not exposed. - visibility: [{ expose: true, types: "Query", fields: "me" }] - ) { - query: Query -} diff --git a/protection/jwt-claims/operations.graphql b/protection/jwt-claims/operations.graphql deleted file mode 100644 index def2613..0000000 --- a/protection/jwt-claims/operations.graphql +++ /dev/null @@ -1,6 +0,0 @@ -query Customer { - me { - id - name - } -} diff --git a/protection/jwt-claims/stepzen.config.json b/protection/jwt-claims/stepzen.config.json deleted file mode 100644 index af1c0ea..0000000 --- a/protection/jwt-claims/stepzen.config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "endpoint": "api/miscellaneous" -} From 85c0c56a73687f2d712af2bbc6da64a17518a003 Mon Sep 17 00:00:00 2001 From: Dan Debrunner Date: Tue, 2 Jul 2024 17:25:24 -0400 Subject: [PATCH 09/10] chore: cleanup --- protection/jwt-claims-dbquery/paging.graphql | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/protection/jwt-claims-dbquery/paging.graphql b/protection/jwt-claims-dbquery/paging.graphql index b2165b0..32186f5 100644 --- a/protection/jwt-claims-dbquery/paging.graphql +++ b/protection/jwt-claims-dbquery/paging.graphql @@ -9,9 +9,6 @@ type Customer { """ `CustomerConnection` is the connection type for `Customer` pagination. -In StepZen, a field returning a connection type must have arguments -`first` (number of nodes to fetch) and `after` (opaque cursor indicating -the starting point). """ type CustomerConnection { edges: [CustomerEdge] @@ -40,10 +37,7 @@ input CustomerFilter { extend type Query { # customers is the exposed field that limits the caller to regions # based upon the regions claim in the request's JWT. - customers( - first: Int! = 10 - filter: CustomerFilter - ): [Customer] + customers(first: Int! = 10, filter: CustomerFilter): [Customer] @sequence( steps: [ { query: "_regions" } @@ -76,7 +70,7 @@ extend type Query { _customers( first: Int! = 10 after: String! = "" - filter: CustomerFilter # regions: [String]! + filter: CustomerFilter regions: [String]! ): CustomerConnection @dbquery( From 1465ad3b4db5597b045fae3478605a3bd1d39614 Mon Sep 17 00:00:00 2001 From: Dan Debrunner Date: Mon, 9 Sep 2024 12:35:47 -0400 Subject: [PATCH 10/10] chore: add testing --- protection/jwt-claims-dbquery/README.md | 7 +- .../jwt-claims-dbquery/operations.graphql | 4 +- protection/jwt-claims-dbquery/paging.graphql | 14 +- protection/jwt-claims-dbquery/tests/Test.js | 171 ++++++++++++++++++ tests/gqltest.js | 4 + 5 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 protection/jwt-claims-dbquery/tests/Test.js diff --git a/protection/jwt-claims-dbquery/README.md b/protection/jwt-claims-dbquery/README.md index 463601c..0b11a95 100644 --- a/protection/jwt-claims-dbquery/README.md +++ b/protection/jwt-claims-dbquery/README.md @@ -8,25 +8,28 @@ to those matching the regions defined in a JWT claim. Run the [sample operations](operations.graphql): JWT with `regions: IN`. + ``` stepzen request -f operations.graphql --operation-name=Customers \ --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJJTiJdfQ.hDi3-qaIOSFKzlFvaXwSh0trXC3vjiOehSKE0OxgOdE" ``` - JWT with `regions: IN, UK`. + ``` stepzen request -f operations.graphql --operation-name=Customers \ - --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJJTiJdfQ.hDi3-qaIOSFKzlFvaXwSh0trXC3vjiOehSKE0OxgOdE" + --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJJTiIsIlVLIl19.CRD85IIMMwjaFebtQ_p3AjSoUM6KtH4gvjcfLQfdmjw" ``` JWT with `regions: US, UK`. + ``` stepzen request -f operations.graphql --operation-name=Customers \ --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJVUyIsIlVLIl19.pf0-A6TN_hT-ldCvsZyqYGv4Twjm9s6wO1aatCjK9Aw" ``` JWT with `regions: US, UK` and user supplied filter + ``` stepzen request -f operations.graphql --operation-name=Customers \ --var f='{"city": {"eq":"London"}}' \ diff --git a/protection/jwt-claims-dbquery/operations.graphql b/protection/jwt-claims-dbquery/operations.graphql index ede5e35..184f063 100644 --- a/protection/jwt-claims-dbquery/operations.graphql +++ b/protection/jwt-claims-dbquery/operations.graphql @@ -1,5 +1,5 @@ -query Customers($f:CustomerFilter) { - customers(filter:$f) { +query Customers($f: CustomerFilter) { + customers(filter: $f) { id name city diff --git a/protection/jwt-claims-dbquery/paging.graphql b/protection/jwt-claims-dbquery/paging.graphql index 32186f5..2246a3c 100644 --- a/protection/jwt-claims-dbquery/paging.graphql +++ b/protection/jwt-claims-dbquery/paging.graphql @@ -34,6 +34,10 @@ input CustomerFilter { city: StringFilter } +type _RegionsList { + regions: [String]! +} + extend type Query { # customers is the exposed field that limits the caller to regions # based upon the regions claim in the request's JWT. @@ -52,7 +56,15 @@ extend type Query { ) # extracts the regions visible to the request from the JWT. - _regions: [String]! @value(script: { src: "$jwt.regions" }) + _regions: _RegionsList + @value( + script: { + src: """ + {"regions": `$jwt`.regions } + """ + language: JSONATA + } + ) # this flattens the customer connection pagination structure # into a simple list of Customer objects. diff --git a/protection/jwt-claims-dbquery/tests/Test.js b/protection/jwt-claims-dbquery/tests/Test.js new file mode 100644 index 0000000..7bcf02b --- /dev/null +++ b/protection/jwt-claims-dbquery/tests/Test.js @@ -0,0 +1,171 @@ +const fs = require("fs"); +const path = require("node:path"); +const { + deployAndRun, + runtests, + GQLHeaders, + endpoint, + getTestDescription, +} = require("../../../tests/gqltest.js"); + +testDescription = getTestDescription("snippets", __dirname); + +const requestsFile = path.join(path.dirname(__dirname), "operations.graphql"); +const requests = fs.readFileSync(requestsFile, "utf8").toString(); + +describe(testDescription, function () { + // just deploy + deployAndRun(__dirname, [], undefined); + + // and then run with various JWTs + runtests( + "regions-in", + endpoint, + new GQLHeaders().withToken( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJJTiJdfQ.hDi3-qaIOSFKzlFvaXwSh0trXC3vjiOehSKE0OxgOdE" + ), + [ + { + label: "customers", + query: requests, + operationName: "Customers", + expected: { + customers: [ + { + id: "10", + name: "Salma Khan ", + city: "Delhi ", + region: "IN ", + }, + ], + }, + }, + ] + ); + runtests( + "regions-in-uk", + endpoint, + new GQLHeaders().withToken( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJJTiIsIlVLIl19.CRD85IIMMwjaFebtQ_p3AjSoUM6KtH4gvjcfLQfdmjw" + ), + [ + { + label: "customers", + query: requests, + operationName: "Customers", + expected: { + customers: [ + { + id: "3", + name: "Salim Ali ", + city: "London ", + region: "UK ", + }, + { + id: "4", + name: "Jane Xiu ", + city: "Edinburgh ", + region: "UK ", + }, + { + id: "10", + name: "Salma Khan ", + city: "Delhi ", + region: "IN ", + }, + ], + }, + }, + ] + ); + runtests( + "regions-us-uk", + endpoint, + new GQLHeaders().withToken( + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI1IiwicmVnaW9ucyI6WyJVUyIsIlVLIl19.pf0-A6TN_hT-ldCvsZyqYGv4Twjm9s6wO1aatCjK9Aw" + ), + [ + { + label: "customers", + query: requests, + operationName: "Customers", + expected: { + customers: [ + { + id: "1", + name: "Lucas Bill ", + city: "Boston ", + region: "US ", + }, + { + id: "2", + name: "Mandy Jones ", + city: "Round Rock ", + region: "US ", + }, + { + id: "3", + name: "Salim Ali ", + city: "London ", + region: "UK ", + }, + { + id: "4", + name: "Jane Xiu ", + city: "Edinburgh ", + region: "UK ", + }, + { + id: "5", + name: "John Doe ", + city: "Miami ", + region: "US ", + }, + { + id: "6", + name: "Jane Smith ", + city: "San Francisco ", + region: "US ", + }, + { + id: "7", + name: "Sandeep Bhushan ", + city: "New York ", + region: "US ", + }, + { + id: "8", + name: "George Han ", + city: "Seattle ", + region: "US ", + }, + { + id: "9", + name: "Asha Kumari ", + city: "Chicago ", + region: "US ", + }, + ], + }, + }, + { + label: "customers-filter", + query: requests, + operationName: "Customers", + variables: { + f: { city: { eq: "London" } }, + }, + expected: { + customers: [ + { + id: "3", + name: "Salim Ali ", + city: "London ", + region: "UK ", + }, + ], + }, + }, + ] + ); +}); diff --git a/tests/gqltest.js b/tests/gqltest.js index a9f5b98..0784cad 100644 --- a/tests/gqltest.js +++ b/tests/gqltest.js @@ -5,6 +5,7 @@ const path = require("node:path"); const { runtests, + GQLHeaders, } = require('gqltest/packages/gqltest/gqltest.js'); const stepzen = require("gqltest/packages/gqltest/stepzen.js"); @@ -59,5 +60,8 @@ function getTestDescription(testRoot, fullDirName) { exports.deployAndRun = deployAndRun; exports.getTestDescription = getTestDescription; +exports.endpoint = endpoint; +exports.GQLHeaders = GQLHeaders; +exports.runtests = runtests; exports.stepzen = stepzen;