I Am Looking for Part 1
You can find the first part of the Build a GraphQL Client Application to Consumer Protected GraphQL API Resources series here.
Overview
We will build the GraphQL API server with express-graphql
and lokijs
as a built-in database.
Our application would be a tweet service that serves and consumes data exposed through APIs as
per GraphQL specification. Once we build the application, we will deploy it to a local
kubernetes cluster using kind
and enforce centralized and decoupled policy based authorization without
modifying any business logic or code.
You can checkout this entire demo application and related integration source here
Build the GraphQL server application
Pre-requisites
Following tools are required to build this application. nodejs
was picked for its simplicity to build apps.
SKIP/JUMP LEVEL
In case you are not interested in building the application from scratch, you can skip some of the steps below and instead checkout/clone the Cloudentity Samples GraphQL Demo GitHub repository to get the application source code.
git clone git@github.com:cloudentity/ce-samples-graphql-demo.git
Then, follow the instructions to continue with the Build and Deploy Section
Initialize a Node.js Project
-
Initialize a Node.js project.
mkdir tweet-service-graphql-nodejs && cd tweet-service-graphql-nodejs npm init
-
Click ENTER with no input for all prompts during the
npm init
and finally typeyes
for the OK prompt. This creates apackage.json
file for the project that holds the dependencies and other execution script commands. -
Add a
start
command to scripts section in thepackage.json
file to start the application quickly, e.g.:{ .. "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node index.js" }, .. }
-
We will use the
express
npm module for HTTP handling, so let’s install the dependency as well.npm install --save express
Add HTTP Routers and Listeners
Create a file named index.js
and add a basic router and listeners.
var express = require('express');
var app = express();
app.get('/', function(req, res) {
res.send("We are building a tweet service!")
});
app.get('/health', function(req, res) {
res.send('Service is alive and healthy')
});
app.listen(5001);
console.log("Server listening at http://localhost:5001/");
Run the Application
Run the application by executing the below command in your terminal:
npm start
This starts and serves the Node.js application listener, and the endpoints below should be serving traffic:
Now that we have the basic structure in place, let’s continue to add some GraphQL specific features to the Node.js application.
Add GraphQL Capabilites
To add GraphQL API compliant endpoint within the Node.js server application,
we will use the express-graphql
npm package.
-
Let’s install the dependencies and attach a listener endpoint for graphQL.
npm install --save graphql@15.3.0 express-graphql@0.12.0
-
Add a schema specification to the
index.js
file.GraphQL SDL allows defintion of the GraphQL schema using various constructs as defined in the GraphQL specification. Let’s build a schema that has a mixed flavor of basic object types, fields, query and mutations.
In the schema, we will add a mix of GraphQL constructs and, later in the article, we will explore how to attach externalized authorization policies to each of these GraphQL constructs.
GraphQL Construct Examples Object Type TweetInput, Tweet Field content, author, id, dateModified, dateCreated .. Query sayHiTweety, getTweet, getLatestTweets .. Mutation createTweet, updateTweet, deleteTweet .. Add below schema specification to
index.js
// graphql package import var { graphqlHTTP } = require('express-graphql'); var { buildSchema } = require('graphql'); // graphql schema definition var schema = buildSchema( ` input TweetInput { content: String author: String } type Tweet { id: String content: String dateCreated: String dateModified: String author: String } type Query { sayHiTweety: String getTweet(id: String!) : Tweet getLatestTweets : [Tweet] } type Mutation { createTweet(tweet: TweetInput): Tweet updateTweet(id: String!, tweet: TweetInput): Tweet deleteTweet(id: String!): String }` );
-
Add implementation & listener for a GraphQL query
Let’s add a simple resolver implementation for the first query
sayHiTweety
. We will expand to other query and mutation implementations later in the article. In contrast to REST, GrpahQL is designed to be a single endpoint API system. All the queries and mutations will be served over this single enpoint at/graphql
. The URL name can be anything,graphql
is just used for convenience.var resolverRoot = { sayHiTweety: () => { return 'Hello Tweety'; } }; app.use('/graphql', graphqlHTTP( { schema: schema, rootValue: resolverRoot, graphiql: true } ));
-
Verify GraphQL API operations.
Start the application using the
npm start
command and launch the GraphQL endpoint at http://localhost:5001/graphql. Since thegraphiql
flag is set totrue
, we will see an interactive query screen and schema explorer as the response.Important
We will not be using above interface but usage of Postman application for further verification of GraphQL APIs is recommended.
You can download the Postman application, in case you don’t have it already, and then import the Cloudentity GraphQL tweet service demo postman collection into Postman.
-
Execute GraphQL
sayHiTweety
query.Navigate to the imported Postman collection under
request-noauth
folder and run thesayHiTweety-Query
GraphQL API request. It should respond with the response attached below.{ "data": { "sayHiTweety": "Hello Tweety" } }
Expand GraphQL Implementation Logic
Let’s add a simple in-memory datastore to store the tweets and then add some mutations and queries to act on those objects. We will not dive into the specifics or add any complex business logic. Our main goal is to showcase externalized authorization policy administration and enforcement for the various GraphQL objects, fields, queries, mutations, and more at runtime.
-
Install the dependencies.
npm install --save uuid lokijs
-
Add implementation for the GraphQL query and mutation.
//imports var loki = require('lokijs'); const {v4: uuidv4} = require('uuid'); //logic var db = new loki('tweets.db'); var tweets = db.addCollection('tweets'); const getTweet = (tid) => { console.log("Fetching record.."); var results = tweets.find({id: tid.id}); var res = results.length > 0 ? results[0] : null return res; } const storeTweet = (t) => { tweets.insert( { id: t.id, content: t.content, author: t.author, dateCreated: t.dateCreated } ); console.log(tweets); return t; } function Tweet(input) { this.id = uuidv4(); this.content = input.tweet.content; this.author = input.tweet.author; this.dateCreated = new Date().toLocaleString(); this.dateModified = new Date().toLocaleString(); } var resolverRoot = { sayHiTweety: () => { return 'Hello Tweety'; }, getTweet: (tid) => { console.log("Fetching tweet using id: " + Object.values(tid)); return getTweet(tid); }, createTweet: (input) => { console.log("Creating a new tweet..."); const newTweet = new Tweet(input); storeTweet(newTweet); return newTweet; }, getLatestTweets: () => { console.log("Fetching records.."); var tweets = db.getCollection('tweets'); var all = tweets.find({ 'id': { '$ne': null } }); return all; }, deleteTweet: (tid) => { console.log("Deleting tweet.."); var tweets = db.getCollection('tweets'); tweets.findAndRemove({id: tid.id}); return tid.id; }, };
-
Verify all GraphQL API operations.
Navigate to the imported collection in Postman and run rest of the GraphQL endpoints. These should now return responses similar to below attached samples. At this point the GraphQL API application is completely unprotected and data can be requested or posted without authorization.
Deploy and Run GraphQL API Rorkload in Kubernetes Cluster
Let’s deploy the GraphQL API onto a local Kubernetes cluster to enforce externalized authorization policies with the Cloudentity authorization platform without any modification to the application code.
Makefile
We will be referencing
make
commands in the below sections. There is a Makefile in the project folder and the contents can be inspected for the actual commands that are orchestrated by themake
target.
Prerequisites
Local Kubernetes cluster
can be deployed using any tool of your choice, but we will use kind
in this article.
SKIP/JUMP LEVEL
In case you want to skip some of the deployment detailed steps and want to move to next logical step with a shortcut, use the following command:
make all
This will deploy all resources and you can jump to the Protect using Cloudentity authorization platform.
Build the Docker Image
-
Get a copy of the
Makefile
andDockerfile
.wget https://raw.githubusercontent.com/cloudentity/ce-samples-graphql-demo/blob/master/tweet-service-graphql-nodejs/Makefile wget https://raw.githubusercontent.com/ce-samples-graphql-demo/blob/master/tweet-service-graphql-nodejs/Dockerfile
-
Now, let’s build the Docker image for the GraphQL API using:
make build-image
Launch the Kubernetes Cluster
We will go ahead and create a Kubernetes cluster using kind
. The below cluster config will be used to
create the cluster and within the config, a NodePort
is configured to enable accessibility
at port 5001
from outside of the cluster since we do not have an actual load balancer.
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 31234
hostPort: 5001
protocol: TCP
-
Get a copy of
k8s-cluster-config.yaml
wget https://raw.githubusercontent.com/cloudentity/ce-samples-graphql-demo/master/tweet-service-graphql-nodejs/k8s-cluster-config.yaml
-
Create the kind cluster using:
make deploy-cluster
Deploy GraphQL Application on the Kubernetes Cluster
We will use Helm (a package manager for Kubernetes) to define and deploy all the Kubernetes resources required for the application. We will not be going into the Helm details, so copy the existing helm templates into our working directory from Cloudentity Samples GraphQL Demo.
Using the below make
command, upload the image to the kind cluster, create a Kubernetes
namespace, and deploy the GraphQL app.
make deploy-app-graph-ns
The above make target launches all the pods and services to run the GraphQL application. The status of the pods and services can be fetched using:
kubectl get pods -n svc-apps-graph-ns
kubectl get services -n svc-apps-graph-ns
Service Readiness
Let’s execute into the pod container to see if the service is reachable
(note the ID appended to the name of the pod after running kubectl get pods -n svc-apps-graph-ns
and replace it in the command below)
kubectl exec -it <pod-name> -n svc-apps-graph-ns -- /bin/sh
Now, you can run the following CURL request:
curl --location --request POST 'http://local.cloudentity.com:5001/graphql' \
--header 'Content-Type: application/json' \
--data-raw '{"query":"query {\n sayHiTweety\n}\n","variables":{}}'
After the request, the application should respond with the following data:
{
"data": {
"sayHiTweety": "Hello Tweety"
}
}
Network Access
The components deployed on the Kubernetes cluster are not exposed outside the Kubernetes cluster. External access to individual services can be provided by creating an external load balancer or node port on each service. An Ingress Gateway resource can be created to allow external requests through the Istio Ingress Gateway to the backing services.
Deploy Istio
To allow external service access, let’s install Istio onto this cluster.
We will use the Helm based install mechanism for installing Istio
We have condensed all required steps under the make
target, which updates the Helm repository
and installs istiod
under the istio-system
namespace.
make deploy-istio
Check the status of the pods:
kubectl get pods -n istio-system
Once the pod is healthy, let’s add an Istio Ingress Gateway to expose the traffic outside the cluster.
Expose the Service Externally with Istio Ingress Gateway
Let’s install the Istio Ingress Gateway
and configure a Virtual Service
for routing to the GraphQL service in the svc-apps-graph-ns
namespace.
-
Let’s first copy some configs that will be used for the below steps from the GitHub repository:
mkdir istio-configs wget -P istio-configs https://raw.githubusercontent.com/cloudentity/ce-samples-graphql-demo/master/tweet-service-graphql-nodejs/istio-configs/istio-helm-config-override.yaml wget -P istio-configs https://raw.githubusercontent.com/cloudentity/ce-samples-graphql-demo/master/tweet-service-graphql-nodejs/istio-configs/istio-ingress-gateway-graphql.yaml wget -P istio-configs https://raw.githubusercontent.com/cloudentity/ce-samples-graphql-demo/master/tweet-service-graphql-nodejs/istio-configs/istio-ingress-virtual-service.yaml wget -P istio-configs https://raw.githubusercontent.com/cloudentity/ce-samples-graphql-demo/master/tweet-service-graphql-nodejs/istio-configs/istio-mp-authorizer-policy.yaml
-
Deploy the Istio Ingress Gateway using:
make deploy-istio-gateway
You can confirm if the gateway and virtual service is created and running using
kubectl get gateways -A kubectl get virtualservices -A
-
Now, let’s check to see if we can access the service from outside the cluster by executing one (or both) of the following commands:
curl --location --request POST 'http://localhost:5001/graphql' \ --header 'Host: local.cloudentity.com' --header 'Content-Type: application/json' \ --data-raw '{"query":"query {\n sayHiTweety\n}\n","variables":{}}'
OR
curl --location --request POST 'http://local.cloudentity.com:5001/graphql' \ --header 'Content-Type: application/json' \ --data-raw '{"query":"query {\n sayHiTweety\n}\n","variables":{}}'
Notice that the either the
Host
header needs to be passed in or the domain needs to have a matching name oflocal.cloudentity.com
as it’s defined in theIstio Virtual Service
routing rule.Expected output:
{ "data": { "sayHiTweety": "Hello Tweety" } }
Result
Voila! Now the services should be accessible from outside the cluster. Now that we have a production-like Kubernetes deployment serving GraphQL operations from the platform, let’s dive into protecting the application using externalized authorization policies using the Cloudentity platform without altering the application code at all.
Authorization Policy Administration in Cloudentity Authorization Platform
Sign up for a free Cloudentity Authorization SaaS account.
Activate the tenant and take the self guided tour to familiarize yourself with the platform.
Now that you have the Cloudentity platform available, let’s connect all the pieces together as shown below:
Annotate Services for Service Auto Discovery
Cloudentity Authorizers can self-discover API endpoints if annotated properly in a Kubernetes cluster. More details on discovery can be found in auto discovery of services on Istio.
For example, in the Helm chart,
we have annotated the services so that final deployment resource file has the annotations.
services.k8s.cloudentity.com/spec-url
annotation enables this GraphQL schema to be read by
Cloudentity Istio Authorizers deployed onto a cluster and then propagated to
the Cloudentity plaform can then govern and
attach declarative policies on the GraphQL schema itself.
Note
The GraphQL schema URL can be served by the GraphQL API resource server itself or hosted in separate accessible location. We are using an external URL only for demonstration purpose.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "tweet-service-graphql-nodejs.fullname" . }}
labels:
{{- include "tweet-service-graphql-nodejs.labels" . | nindent 4 }}
annotations:
services.k8s.cloudentity.com/spec-url: "https://raw.githubusercontent.com/cloudentity/random-bin/master/graphql/tweet-svc-schema"
services.k8s.cloudentity.com/graphql-path: "/graphql"
In case you want to change the annotation, you can update the url in the Helm template and do the following:
helm uninstall svc-apps-graphql -n svc-apps-graph-ns
helm install svc-apps-graphql helm-chart/tweet-service-graphql-nodejs -n svc-apps-graph-ns
Deploy Cloudentity Istio Authorizer to the Kubernetes Cluster
In this step, we will install and configure the Cloudentity Istio Authorizer to act as the Policy Decision Point(PDP). The scope of responsibility of this component is to act as the local policy decision point within the Kubernetes cluster. This component is also responsible to pull down all the applicable authorization policies authored and managed within the Cloudentity authorization platform. In the below image, the highlighted section in the box is the component that we will download and install onto a local Kubernetes cluster.
Detailed Istio setup concepts and instruction are available here, but, in a nutshell, the steps are:
-
Navigate to the Cloudentity authorization platform admin console.
-
Go to Authorization » Gateways, select Create Gateway, and create a new Istio authorizer.
-
Select Create and bind services automatically.
Technically, this means we will register this discovered service as an
OAuth resource server
within the Cloudentity platform. -
Install the
istio-authorizer
in target Kubernetes using thehelm
commands provided in the Quickstart installation instructions Step 1 (make sure to select the visibility icon to make the secrets visible before copying the commands).You will also need to edit the
helm upgrade
command to add the namespacesvc-apps-graph-ns
to the list of discovery namespaces, as shown below:helm upgrade --install istio-authorizer acp/istio-authorizer \ ... --set "discovery.namespaces={default,svc-apps-graph-ns}"\ ...
Note
Cloudentity Istio Authorizer will be deployed to its own name space (in this case
acp-istio-authorizer
). The authorizers can be deployed to any namespace based on the deployment architecture. The namespaces chosen in this article are for demonstration purpose only.What happens with the given helm command? It:
- Creates a new
acp-istio-authorizer
namespace - Deploy the
istio-authorizer
under that namespace - Configures the target namespaces the authorizer should scan for service discovery and traffic enforcement
- Creates a request body parser Envoy filter resource for the service pod in the target namespaces (if configured).
- Creates the Cloudentity authorization policy resource as an Istio external authorization policy. External authorization policies are “CUSTOM” actions and will be evaluated first in the Istio authorization policy authorizer.
You can modify the command to include an override in the values file
parseBody: enabled: true
helm upgrade --install istio-authorizer acp/istio-authorizer \ -f overide-values.yaml .. ..
Important
If you don’t apply the above request parser sidecar, you will get the following error:
{ "errors": [ { "message": "failed to parse json: unexpected end of JSON input" } ] }
- Creates a new
-
Attach external authorization to Istio:
Cloudentity Istio Authorizer is designed to be a native Istio extension that uses the Istio External authorizer model. Following the Step 2 instructions under the Quickstart tab, add
extensionProviders
undermesh
section to indicate thatacp-authorizer
will be an external authz provider. -
Confirm if your authorizer deployment is healthy:
kubectl get pods -n acp-istio-authorizer
Important
We have seen issues with this step if there is network traffic restrictions between the local workstations and the external Cloudentity platform due to internal firewalls, etc. Make sure the traffic path is allowed in case you see the pod status as not healthy.
Restart the Service Pods
Since we are using automatic Istio injection, we need to recreate the pod so that envoy-proxy
is
injected into the service namespaces:
kubectl rollout restart deployment/svc-apps-graphql-tweet-service-graphql-nodejs -n svc-apps-graph-ns
After this step we should see the APIs auto discovered by the Cloudentity Istio Authorizer and propagated back up to the the Cloudentity Authorization SaaS platform. Let’s check it out in the Cloudentity authorization platform.
Cloudentity Authorizer and Cloudentity Platform Communication
It is very important that the communication path between the local Istio Authorizer and Cloudentity Authorization SaaS platform is established. The local Istio Authorizer is responsible for pushing any discovered services to platform for further governance. Once pushed, the centralized policies administered and managed in the platform are polled back by the authorizer for policy decisions and enforcement locally.
Service Communication
-
Login into the Cloudentity authorization admin portal.
-
Navigate to the Authorization » Gateways.
As shown in the below diagram, the Last Active column is an indication of communication status of the local authorizer with the remote platform.
Regarding the communication security, the local Istio authorizer uses
OAuth
authorization mechanisms to authenticate itself to the Cloudentity authorization SaaS platform before handshaking information.
Discovered Service from Cluster
If the business service(workload) is discovered from the remote Kubernets cluster, it should
automatically be bound in the Cloudentity platform. Technically, this means
Cloudentity authorization platform registers this discovered service as an
OAuth resource server
and can be governed from within the platform).
Govern GraphQL API and Schema
At this point, the remote workload should be available to be governed within the Cloudentity authorization platform. In case of the GraphQL workload, the schema annotated along with the workload is also transferred by the local Cloudentity Istio Authorizer to the Cloudentity authorization platform. This enables us to explore the GraphQL schema for the workload and we can apply authorization policies and manage the policies applied to the various constructs within the GraphQL schema.
Now we have seen the service is available in the Cloudentity authorization platform for governance and central management of authorization policies, which will automatically be downloaded by respective local satelite Cloudentity authorizers bound to the services. This way, the Cloudentity authorization platform acts as a very powerful and robust policy management services engine and the Cloudentity authorizers acts as policy runtime services that makes decisions using the policies governed and administered within the central policy management engine.
Enforce externalized dynamic authorization
Before we start enforcing policies, run the postman collection to check if all the GraphQL API operations are still accessible.
Once the pre check is complete, let’s try to add more authorization scenarios to enforce access and authorization policies authored and managed via the Cloudentity authorization platform.
For policy governance, we expect the admin (policy administrator) to:
-
Login into the Cloudentity Authorization portal.
-
Navigate to the workspace and select Enforcement > APIs to see the GraphQL APIs.
-
Apply policies at various GraphQL construct level in the GraphQL API explorer.
Scenario #1: Block GraphQL endpoint alltogether
Let’s say we want to temporarily block access to all users for this endpoint.
Select the GraphQL API endpoint and attach a Block API
policy.
This, in effect, blocks any call made to any GraphQL operation. Run any of the tests in the imported Postman Test collection and we should see an “authorization denied” response.
Authorization policy | Output |
---|---|
Scenario #2: Disallow GraphQL Queries for Specific Fields
Let’s say we do not want GraphQL clients to ask for a specific field that is available in the schema. These could be internal identifiers or only could be requested by some special priviliged internal applications. For this:
-
Select the GraphQL API endpoint and enter into the GraphQL API explorer.
-
Navigate to
Objects
and go to theTweet
field. -
Assign the
Block API
policy todateModified
completely.
This, in effect, blocks any GraphQL query
that requests for the dateModified
field.
Let’s run the getLatestTweets
query from the imported Postman Test collection.
Modify the request payload to include/exclude dateModified
in the query and observe the response difference.
Whenever dateModified
is requested, the request is automatically rejected.
Authorization policy | Output |
---|---|
Scenario #3: Disallow GraphQL Queries for Specific Objects with Constraints
Let’s say we do not want GraphQL clients to ask for a specific object unless some specific
constraints are met for that client application.
For example, we want to return the Tweet
object only when some constraint
is met.
For this, we can apply a policy at the object level
for IP address check that is available in the given range. We will use the inbuilt REGO
engine to author
the policy.
For this:
-
Select the GraphQL API endpoint and enter into the GraphQL API explorer
-
Navigate to Objects
-
Select the
Tweet
object and apply the constrainedIP check
policy at object level.
Sample Rego policy:
package acp.authz
default allow = false
allowedCidrRange :=
[
"3.0.0.0/9",
"3.128.0.0/10",
"4.0.0.0/8",
"5.8.63.0/32",
"5.10.64.160/29",
"217.161.27.0/25"
]
extracted_ip := input.request.headers["X-Custom-User-IP"][_]
is_within_allowed_cidr = true {
some i
net.cidr_contains(allowedCidrRange[i], extracted_ip)
} else = false
allow {
is_within_allowed_cidr
}
Authorization policy | Output |
---|---|
Scenario #4: Block GraphQL Delete Mutation Unless It Comes from Client with Specific Metadata
Let’s say we do not want all GraphQL clients to operate on the deleteTweet
mutation.
For example, we want to allow only access tokens issued to specific clients to be authorized to use the
deleteTweet
mutation. This way this operation can be used by specific clients and not all client
apps even though it is available in schema.
For this:
-
Select the GraphQL API endpoint and enter into the
GraphQL API explorer
-
Navigate to
Mutation
-
Select the
deleteTweet
Mutation operation and apply the constrainedallow-only-for-specific-clients
policy at mutation level.
Authorization policy | Output |
---|---|
Scenario #5: Allow GraphQL Query Only if Token Is Issued by Specific Authorization Server
Let’s say we do not want all GraphQL clients to operate on the getTweets
query.
For example, we want to allow only access tokens issued by specific authorization servers to be
authorized to use the getTweets
query.
Authorization policy | Output |
---|---|
Next steps
Now that we have protected a GraphQL API resource server with dynamic and flexible authorization policies, we will build a simple GraphQL client application to demonstrate an entire application in real life. In this client application, we will look at how to get an access token from the Cloudentity authorization server and, then, utilize it to make further calls to the GraphQL API resource server.