- Description
- Key Components
- Architecture
- Building the library and callout
- Annotating GraphQL schema with scopes
- Using the library in an Apigee Proxy
- Using the JavaScript Callout in an Apigee Proxy
- Library Functions Reference
This repo provides tooling to enable GraphQL query authorization in Apigee Edge. We also provide a CLI tool that makes it easy at build time to extract an authorization mapping between scopes and entitlements from a GraphQL schema. For run-time, we provide an Apigee JavaScript callout that can be used in combination with the authorization mapping to authorize a GraphQL query.
This module uses the graphql.js library to provide the following:
- A JavaScript GraphQL library (graphql.lib.js) that can be used within other Apigee's JavaScript callouts.
- A standalone Apigee JavaScript callout (graphql.jsc.js) that can be used to authorize GraphQL queries before proxying to a backend GraphQL server.
- A command (gql-s2e) for converting GraphQL schemas to a map of scopes ⇨ entitlements.
This is the architecture used to enable the above 3 components.
This step builds both the graphql.lib.js, and graphql.jsc.js files. The output is under the dist directory.
$ npm run build
By default, it produces uglify-ed/minify-ed outputs. If you want pretty-fied output use:
$ npm run build-pretty
To make gql-s2e available to run in your path, run the following
$ npm link
By running this, a link is created from the bin directory of your Node.js installation to the location where you have cloned this repo.
Below is a sample of how you can add scopes using the @scope
directive in your GraphQL schema.
The @scope
directive is a custom directive in this project.
# sample schema from Apollo Server (https://www.apollographql.com/docs/graphql-tools/generate-schema.html)
directive @scope(value: String) on OBJECT
type Author {
id: Int!
firstName: String
lastName: String
"""
the list of Posts by this author
"""
posts: [Post]
}
type Post {
id: Int!
title: String
author: Author
votes: Int
}
# the schema allows the following query:
type Query {
posts: [Post] @scope (name:"scopea", query: true)
author(id: Int!): Author @scope (name:"scopeb", query: true)
}
# this schema allows the following mutation:
type Mutation {
upvotePost (
postId: Int!
): Post @scope (name: "scopec", mutation: true)
}
Using the schema above, run the following command: gql-s2e yourfile.graphql
.
Here is a sample output:
{
"scopea": [
"query.posts"
"query.posts.**"
],
"scopeb": [
"query.author"
"query.author.**"
],
"scopec": [
"mutation.upvotePost"
"mutation.upvotePost.**"
]
}
1. Create JavaScript Resource: Use the Apigee UI or the Management Server API to create a new JavasScript resource that is the content of graphql.lib.js
.
2. Create JavaScript policy: Create a new JavaScript policy and reference the graphql.lib.js
file using tag.
<IncludeURL>jsc://graphql.lib.js</IncludeURL>
3. Use the exported functions: Within the source of the JavaScript policy from step 2, you can now make use of the functions under the graphql.*
object.
1. Create JavaScript policy: Create a new JavaScript policy, using the contents of the dist/graphql.jsc.js file.
2. Set the Callout properties: Within the XML configuration of the JSC Callout (from step 1), set the callout properties.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Javascript async="false" continueOnError="false" enabled="true" timeLimit="200" name="Authz">
<DisplayName>Authz</DisplayName>
<Properties>
<Property name="input">{query}</Property>
<Property name="entitlements">{entitlements}</Property>
<Property name="debug">true</Property>
</Properties>
<ResourceURL>jsc://graphql.jsc.js</ResourceURL>
</Javascript>
If authorization succeeds, the JavaScript Callout completes silently. If authorization fails, the JavaScript Callout throws an error.
The following library functions are exported within the graphql
object made available in from dist/graphql.lib.js
:
This function takes an input query or mutation and checks that all the paths are satisfied by the given list of entitlements.
The return value is an empty array [], if all the given entitlements satisfy the paths being accessed in the input query/mutation. Otherwise, it returns the list of paths, from the input mutation/query, which have not been satisfied.
Each entitlement itself is structured as a dot separated hierarchy of Fields
e.g.
query.GrandParent.Parent.Child
You can use a single wildcard as place-holder for a single field
e.g.
query.GrandParent.*.Child
(This would match the Child field who has a GrandParent, two levels up)
You can use double wildcard as placeholder for multiple fields
e.g.
query.GrandParent.**
(This would match any hierarchy that starts with GrandParent)
Entitlements for queries begin with "query.", and entitlements for mutations begin with "mutation.".
This function takes an input query/mutation and parses it, thus verifying it's syntactically correct.
The return value is the AST representing the query/mutation itself. If an error occurs, the function throws a GraphQLError.
This function takes an input query/mutation, and flattens to convert it to a list of paths.
e.g. This query:
{
employee(id: "foo") {
profile {
fname,
last
}
}
}
Results in the following list of paths:
[
"query.employee.profile.fname"
"query.employee.profile.last"
]
This function derives the entitlements from a GraphQL schema that is annotated with @scope directives.
The location of the @scope directive indicates the paths that are accessible by the given scope.
The @scope directive to a GraphQL type, or a field within a type. Depending on where you put the @scope directive, it has different meaning.
-
If the @scope directive appears on a GraphQL type, then it applies to all fields of this GraphQL type (NOTE: this is not the same as all the fields within the GraphQL type)
-
If the @scope directive appears on a field, then it applies only the field itself (regardless of the GraphQL type)
The @scope directive is defined as follows:
directive @scope(name: String!, query: Boolean = false, mutation: Boolean = false, cascade: Boolean = true)
where
- name - Name of the scope
- query - Whether or not the scope is relevant to queries
- mutation - Whether or not the scope is relevant to mutations
- cascade - Whether or not the scope is applicable to the descendants of the field.
The return value of this function is a map of scopes to entitlements.
e.g.
{
"scopeA": [ "query.employee","query.employee.**"],
"scopeB" : [ "query.company","query.company.**"]
}
This is not an officially supported Google product.