GraphQL API
Documentation for the GraphQL API
The GraphQL API is currently available with v2.3.x-alpha releases. Until the final release, it should be used for testing purposes only.
The Hasura container is intentionally left out of production for the time being. It is only built and brought up in local/development environments with local.yml. Once it is secured and tested, Hasura will be added to production.yml with a production Dockerfile.
This documentation is a work in progress and published for visibility and feedback.

Introduction

Starting in v2.3.0, Ghostwriter includes a Docker container for Hasura used to manage access to the back-end PostgreSQL database via GraphQL.
If the HASURA_GRAPHQL_ENABLE_CONSOLE environment variable is set to true, t, yes, or y, the Hasura console is available on port 8080. The console is where administrators can manage role-based access controls and other configurations.
The console is useful for crafting queries and experimenting.
Hasura is connected directly to the PostgreSQL database! Changes made in the Hasura console take immediate effect. Changing the schema or deleting data will irreversibly change your database and could render Ghostwriter unusable.
In most situations, administrators should leave configurations alone and only use the console for experimenting with the GraphQL requests and possibly adjusting user role permissions.
Once done, disable the console access to better protect against unauthorized access to your database. It can be easily re-enabled by updating the env variable.

In-Development Limitations

While the GraphQL API is still in development there are some limitations to be aware of during testing:
  • Mutations that create new entries for models with default values will fail
    • Primarily affects notes (default NOW() timestamp), but there may be other cases
    • These models need migrations that are in development
  • Client and project invitations can only be managed via the Django admin panel or the API

Interacting with the API

With the default configuration, the GraphQL endpoints are:
Unlike a REST API, a GraphQL API does not have specific endpoints you must query with a particular HTTP request type to receive a predetermined set of results. You submit queries with POST requests to one of the above endpoints as JSON. The JSON includes your personalized query and the data you selected to get back. That means you can get exactly what you need without making multiple requests or parsing extra data.
A standard query is submitted with Content-Type: application/json in the headers and a body like this:
1
{
2
"query": "...",
3
"operationName": "...",
4
"variables": { "foo": "bar", ... }
5
}
Copied!
The query and operationName keys are required. The operationName tells GraphQL which action should be executed. This can be helpful if you stack multiple queries and mutations in the query key and want to selectively execute them (see the example at the bottom of the page).
The response will always come back in this form:
1
{
2
"data": { ... },
3
"errors": [ ... ]
4
}
Copied!
For more information, review the GraphQL documentation on queries:
Queries and Mutations | GraphQL
graphql
Some basic GraphQL knowledge, such as what the difference between a query and a mutation is, will make the following sections easier to understand. You will also be better prepared to write your own custom queries.

Basic Queries

A basic query looks like this:
1
query MyQuery {
2
client {
3
id
4
}
5
}
6
Copied!
It identifies itself as a query with an arbitrary name, states which table it wants to query, and what fields it wants to be returned. This query would return the id field of all client records accessible to the requesting user.
The query can be modified to return additional data, like the codename and note fields:
1
query MyQuery {
2
client {
3
id
4
codename
5
note
6
}
7
}
8
Copied!
Field names are often placed on their own lines in GraphQL examples and in the Hasura Console, but this is not required. You can separate field names with spaces, too. This option is easier to use when preparing queries for web requests because it removes the need to convert newlines to \n.
Queries can also request related data. For a client, you might request the contact information for all related points of contact:
1
query MyQuery {
2
clients {
3
id
4
codename
5
note
6
contacts {
7
email
8
}
9
}
10
}
Copied!
You can include multiple queries in a single request. Here we add a query to fetch the id and title of every finding in the database to get all our data back in a single request:
1
query MyQuery {
2
clients {
3
id
4
codename
5
note
6
contacts {
7
email
8
}
9
}
10
finding {
11
id
12
title
13
}
14
}
Copied!
Finally, you might want to try to take the result of one query and use it as a variable for a subsequent query. When GraphQL receives multiple queries, like in the above example, GraphQL resolves all queries simultaneously, so the results of one cannot feed into another.
In most cases, you can accomplish your goal with a single query. Always remember, you can leverage database relationships tracked in Hasura.
For this final example, assume you want to get the title and severity of every finding ever associated with a particular client's projects where the title contains SMB. This can be accomplished with nested database relationships and the addition of a condition:
1
query MyQuery {
2
clients {
3
projects {
4
reports {
5
reportedFindings(where: {title: {_like: "%SMB%"}}) {
6
title
7
severity {
8
severity
9
}
10
}
11
}
12
}
13
}
14
}
Copied!
Note how the above example references the severity relationship, instead of returning the findings severityId field. The severityId is just the foreign key, an integer. The query uses the relationship to return the string value set to represent that severity (e.g., High).

Interacting via Automation

Queries are simple until you need to pack them into the nested JSON for a web request. You should use a script to craft the proper payloads and make them repeatable.
If you are using the Hasura console to build your query you can click the Code Exporter button to view the query in a code snippet. Hasura generates code for JavaScript, TypeScript, and the Apollo Client right now, but the examples can be easily adapted to another language.
You can write your query in a human-readable format and then use something like JavaScript's JSON.stringify() or Python's json.dumps() to create the properly formatted payload for the POST request.
Here is an example query request in Python. Note how the query variable contains a mutation and a query named Login and Whoami respectively. GraphQL executes the operation named in the operationName key in the requests JSON payload.
1
import json
2
import requests
3
4
5
headers = {"Content-Type": "application/json", }
6
7
def prepare_query(query, operation):
8
return json.dumps({
9
"query": query,
10
"operationName": operation
11
})
12
13
def post_query(headers, data):
14
return requests.post(
15
"http://localhost:8080/v1/graphql",
16
headers=headers,
17
data=data
18
)
19
20
# Stacked query with `Login` and `Whoami` operations
21
query = """
22
mutation Login {
23
login(password:"sp3ct3rops", username:"benny") {
24
token expires
25
}
26
}
27
28
query Whoami {
29
whoami {
30
username role expires
31
}
32
}
33
"""
34
35
# Send query and set `Login` as the `operationName`
36
response = post_query(headers, prepare_query(query, "Login"))
37
# Get the JWT from the response and add it to the headers
38
token = response.json()["data"]["login"]["token"]
39
headers["Authorization"] = f"Bearer {token}"
40
# Send the query again but execute the `Whoami` operation this time
41
response = post_query(headers, prepare_query(query, "Whoami"))
42
# Print our JWT's whoami informaiton
43
print(response.json())
44
Copied!