Skip to main content

GraphQL API

What is GraphQL

"GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools."

— graphql.org

Integrates GraphQL implementation

Integrates currently uses the Ariadne library.

All GraphQL queries are directed to a single endpoint, which is exposed at /api.

The API layer was inspired by GitLab's GraphQL API and is structured in the following way:

api
+-- enums
| +-- __init__.py <- Index for all enum bindings
| +-- enums.graphql <- Available enums schema
+-- interfaces
| +-- name.graphql <- Interface type definition
+-- mutations
| +-- mutation_name.py <- Mutation implementation
| +-- schema.graphql <- Available mutations schema
| +-- inputs
| | +-- schema.graphql <- Available input types schema
| +-- payloads
| | +-- schema.graphql <- DTOs and payloads schema
+-- resolvers
| +-- entity_name
| | +-- field_name.py <- Resolver implementation
+-- scalars
| +-- __init__.py <- Index for all scalar bindings
| +-- scalar_name.py <- Scalar implementation
| +-- scalars.graphql <- Available scalars schema
+-- subscriptions
| +-- subscription_name.py <- Subscription implementation
| +-- schema.graphql <- Available subscriptions schema
+-- unions
| +-- __init__.py <- Type bindings
| +-- root.graphql <- Available union types schema
+-- validations
| +-- validator_name.py <- Validators at the schema level

GraphQL Playground

The GraphQL Playground is a tool that allows to perform queries to the API or to explore the schema definitions in a graphic and interactive way. You can access it on:

Types

Integrates GraphQL types can be found on Documentation Explorer section at the Fluid Attacks API Playground or you can go to the source code.

There are two approaches to defining a GraphQL schema:

  1. Code-first
  2. Schema-first

We use the latter, which implies defining the structure using GraphQL SDL (Schema definition language) and binding it to python functions.

e.g:

# api/resolvers/stakeholder/schema.graphql

type Stakeholder {
"Stakeholder email"
email: String!
}
# api/resolvers/stakeholder/schema.py

from ariadne import (
ObjectType,
)

STAKEHOLDER = ObjectType('Stakeholder')

Enums

Integrates GraphQL enums can be found on Documentation Explorer section at the Fluid Attacks API Playground or you can go to the source code.

# api/enums/enums.graphql

enum AuthProvider {
"Bitbucket auth"
BITBUCKET
"Google auth"
GOOGLE
"Microsoft auth"
MICROSOFT
}
note

By default, enum values passed to resolver functions will match their name

To map the value to something else, you can specify it in the enums binding index, e.g:

# api/enums/__init__.py
from ariadne import EnumType

ENUMS: Tuple[EnumType, ...] = (
...,
EnumType(
'AuthProvider',
{
'BITBUCKET': 'bitbucket-oauth2',
'GOOGLE': 'google-oauth2',
'MICROSOFT': 'azuread-tenant-oauth2'
}
),
...
)

Scalars

Integrates GraphQL scalars can be found on Documentation Explorer section at the Fluid Attacks API Playground or you can go to the source code.

GraphQL provides some primitive scalars, such as String, Int and Boolean, but in some cases, it is required to define custom ones that aren't included by default due to not (yet) being part of the spec, like Datetime, JSON and Upload.

Further reading:

Resolvers

Integrates GraphQL resolvers can be found on GraphiQL Explorer section at the Fluid Attacks API Playground or you can go to the source code.

A resolver is a function that receives two arguments:

  • Parent: The value returned by the parent resolver, usually a dictionary. If it's a root resolver this argument will be None
  • Info: An object whose attributes provide details about the execution AST and the HTTP request.

It will also receive keyword arguments if the GraphQL field defines any.

# api/resolvers/stakeholder/schema.graphql

type Stakeholder {
...
"User email"
email: String!
...
}
# api/resolvers/stakeholder/email.py

from graphql.type.definition import (
GraphQLResolveInfo
)

from typing import (
TypedDict,
Unpack,
)

class ResolveArgs(TypedDict):
email: str

@STAKEHOLDER.field("email")
def resolve(parent: Item, info: GraphQLResolveInfo, **kwargs: Unpack[ResolveArgs]):
return '[email protected]'

The function must return a value whose structure matches the type defined in the GraphQL schema

warning

Avoid reusing the resolver function. Other than the binding, it should never be called in other parts of the code

tip

Further reading:

Mutations

Integrates GraphQL mutations can be found on GraphiQL Explorer section at the Fluid Attacks API Playground or you can go to the source code.

Mutations are a kind of GraphQL operation explicitly meant to change data.

note

Mutations are also resolvers, just named differently for the sake of separating concerns, and just like a resolver function, they receive the parent argument (always None), the info object and their defined arguments

Most mutations only return {'success': bool} also known as SimplePayload, but they aren't limited to that. If you need your mutation to return other data, just look for it or define a new type in api/mutations/payloads/schema.graphql and use it.

# api/mutations/schema.graphql

type Mutation {
...
"Adds a new Stakeholder"
addStakeholder(
"Stakeholder email"
email: String!
"stakeholder role"
role: StakeholderRole!
): AddStakeholderPayload!
...
}
# api/mutations/add_stakeholder.py

from graphql.type.definition import (
GraphQLResolveInfo
)

from typing import (
TypedDict,
Unpack,
)

class AddStakeholderArgs(TypedDict):
email: str
role: str

@MUTATION.field("addStakeholder")
async def mutate(
_parent: None,
info: GraphQLResolveInfo,
**kwargs: Unpack[AddStakeholderArgs],
):
user_domain.create(
kwargs["email"],
kwargs["role"],
)
return AddStakeholderPayload(success=True)

Subscriptions

Integrates GraphQL subscriptions can be found on GraphiQL Explorer section at the Fluid Attacks API Playground or you can go to the source code.

Subscriptions are long-lasting operations designed to provide real-time data updates through bidirectional WebSocket communication. They are commonly used to query AI models and suggest solutions for identified risks.

They can maintain an active connection to your GraphQL server. Typical resolvers are used to start a connection.

note

These subscriptions consist of two key components: the generator, which progressively provides values as they become available, and the resolver, which processes these values, potentially returning them unchanged or with modifications as needed.

# api/subscriptions/schema.graphql

type Subscription {
...
"Suggested fix for the vulnerability"
getCustomFix(
"The id off the vulnerability"
vulnerabilityId: String!
): String!
...
}
# api/subscriptions/get_custom_fix.py

from graphql.type.definition import (
GraphQLResolveInfo
)

from typing import (
AsyncGenerator,
cast,
)

@SUBSCRIPTION.source("getCustomFix")
def generator(_parent: None, info: GraphQLResolveInfo, AsyncGenerator[str, None]):
return "test"

@SUBSCRIPTION.field("getCustomFix")
def resolve(count: str, _info: GraphQLResolveInfo, **_kwargs: str):
return count
tip

Errors

All exceptions raised, will be reported in the "errors" field of the response.

Raising exceptions can be useful to enforce business rules and report back to the client in cases the operation could not be completed successfully.

Further reading:

Authentication

The Integrates API enforces authentication by checking for the presence and validity of a JWT in the request cookies or headers.

For resolvers or mutations that require authenticated users, decorate the function with the @require_login from decorators.

Authorization

The Integrates API enforces authorization implementing an ABAC model with a simple grouping for defining roles. You can find the model here.

Levels and roles

An user can have one role for each one of the three levels of authorization:

  • User
  • Organization
  • Group

Each role is associated with a set of permissions.

Also, Service level exists and it checks the covered features according to group plan like Advanced or Essential.

Enforcer

An enforcer is an authorization function that checks if the user can perform an action on the context.

We define enforcers for each authorization level. Read the description for understanding how to use them.

Boundary

The general methods for listing and getting the user permissions (and the permissions that user can grant) are in boundary.

The whole application must use this methods for implementing controls.

Policy

The general methods for get user role, grant permissions or revoke them, are in policy.

Decorators

For resolvers or mutations that require authorized users, decorate the function with the appropriate decorator from decorators

  • @enforce_user_level_auth_async
  • @enforce_organization_level_auth_async
  • @enforce_group_level_auth_async

Guides

Adding new fields or mutations

  1. Declare the field or mutation in the schema using SDL
  2. Write the resolver or mutation function
  3. Bind the resolver or mutation function to the schema

Editing and removing fields or mutations

When dealing with fields or mutations that are already in use by clients, it's crucial to ensure backward compatibility to prevent breaking changes. To achieve this, we implement a deprecation policy, providing users with advance notice of any planned edits or removals.

This involves informing API users about which fields or mutations will be edited/deleted in the future, granting them adequate time to adapt to this changes.

We use field and mutation deprecation for this. Our current policy mandates removal 6 months after marking fields and mutations as deprecated.

Deprecating fields

To mark fields or mutations as deprecated, use the @deprecated directive, e.g:

type ExampleType {
oldField: String @deprecated(reason: "reason text")
}

The reason should follow something similar to:

This {field|mutation} is deprecated and will be removed after {date}.

If it was replaced or there is an alternative, it should include:

Use the {alternative} {field|mutation} instead.

Dates follow the AAAA/MM/DD convention.

Additionally, we offer the option to assume the risk of using deprecated fields or mutations by including a flag in the commit message. This allows developers to make informed decision when incorporating changes that may affect their implementations.

Removing fields or mutations

When deprecating fields or mutations for removal, these are the common steps to follow:

  1. Mark the field or mutation as deprecated.
  2. Wait six months so clients have a considerable window to stop using the field or mutation.
  3. Delete the field or mutation.

e.g:

Let's remove the color field from type Car:

  1. Mark the color field as deprecated:

    type Car {
    color: String
    @deprecated(
    reason: "This field is deprecated and will be removed after 2022/11/13."
    )
    }
  2. Wait until one day after given deprecation date and Remove the field:

    type Car {}

Editing fields or mutations

When renaming fields, mutations or already-existing types within the API, these are the common steps to follow:

  1. Mark the field or mutation you want to rename as deprecated.
  2. Add a new field or mutation using the new name you want.
  3. Wait until one day after given deprecation date.
  4. Remove the field or mutation that was marked as deprecated.

e.g:

Let's make the color field from type Car to return a Color instead of a String:

  1. create a newColor field that returns the Color type:

    type Car {
    color: String
    newColor: Color
    }
  2. Mark the color field as deprecated and set newColor as the alternative:

    type Car {
    color: String
    @deprecated(
    reason: "This field is deprecated and will be removed after 2022/11/13. Use the newColor field instead."
    )
    newColor: Color
    }
  3. Wait until one day after given deprecation date and remove the color field:

    type Car {
    newColor: Color
    }
  4. Add a new color field that uses the Color type:

    type Car {
    color: Color
    newColor: Color
    }
  5. Mark the newColor field as deprecated and set color as the alternative:

    type Car {
    color: Color
    newColor: Color
    @deprecated(
    reason: "This field is deprecated and will be removed after 2022/11/13. Use the color field instead."
    )
    }
  6. Wait until one day after given deprecation date and remove the newColor field:

    type Car {
    color: Color
    }
note

These steps may change depending on what you want to do, just keep in mind that keeping backwards compatibility is what really matters.