Skip to content

API

Integrates implements a GraphQL API that allows our products and customer-build external integrations to query data and perform mutations exposed in its schema.

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

Structure

The API is built using a schema-first approach enabled by the Ariadne library, structured as follows:

Explorer

The API provides a web-based GUI, that allows performing queries and exploring 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
}

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 ResolverArgs(TypedDict):
email: str
@STAKEHOLDER.field("email")
def resolve(parent: Item, info: GraphQLResolveInfo, **kwargs: Unpack[ResolverArgs]):
return 'test@fluidattacks.com'

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

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.

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.

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

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
    }