Integrates API | GraphQL Schema & Endpoints | Fluid Attacks Help

API

Integrates implements a GraphQL API that allows our products and customers to 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:
โ–ถ ๐Ÿ“ api
โ–ถ ๐Ÿ“ enums
__init__.py       Python mappings
schema.graphql       Enum types
โ–ถ ๐Ÿ“ explorer
__init__.py       Explorer config
โ–ถ ๐Ÿ“ inputs
schema.graphql       Input types
โ–ถ ๐Ÿ“ interfaces
schema.graphql       Interface types
โ–ถ ๐Ÿ“ mutations
mutation_name.py       Python implementation
schema.py       Python bindings
schema.graphql       Mutation type
โ–ถ ๐Ÿ“ payloads
payload_name.py       Python implementation
schema.graphql       Mutation return types
โ–ถ ๐Ÿ“ resolvers
__init__.py       Python bindings
โ–ถ ๐Ÿ“ object_name
field_name.py       Python implementation
schema.py       Python bindings
schema.graphql       Object type
โ–ถ ๐Ÿ“ query
schema.graphql       Query type
โ–ถ ๐Ÿ“ scalars
__init__.py       Python bindings
scalar_name.py       Python implementation
scalars.graphql       Scalar types
โ–ถ ๐Ÿ“ subscriptions
subscription_name.py       Subscription implementation
schema.py       Python bindings
schema.graphql       Subscription type
โ–ถ ๐Ÿ“ unions
__init__.py       Python bindings
union_name.py       Python implementation
schema.graphql       Union types
โ–ถ ๐Ÿ“ validations
validation_name.py       Validation implementation

Explorer

The API provides a web-based GUI that allows performing queries and exploring schema definitions graphically and interactively. You can access it on:
  1. https://app.fluidattacks.com/api, which is production.
  2. https://<branch>.app.fluidattacks.com/api, which are the ephemeral environments (where <branch> is the name of your Git branch).
  3. https://localhost:8001/api, which is local (useful for development).

Types

Integrates GraphQL types can be found on the 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 the 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
)
Notes
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 the 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.

Idea
Tip
Further reading:

Resolvers

Integrates GraphQL resolvers can be found on the 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:
  1. Parent: The value returned by the parent resolver, usually a dictionary. If itโ€™s a root resolver, this argument will be None
  2. 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.
Warning
Caution
Avoid reusing the resolver function. Other than the binding, it should never be called in other parts of the code
Idea
Tip
Further reading:
  1. Ariadne docs - resolvers

Mutations

Integrates GraphQL mutations can be found on the 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.
Notes
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
from  .schema import SECURE_MUTATION
from  integrates.api.auth import  AuthzUserAccess

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

@SECURE_MUTATION.field(" addStakeholder", authz=[AuthzUserAccess()])
async def mutate(
   _parent: None,
   info: GraphQLResolveInfo,
   **kwargs: Unpack[AddStakeholderArgs],
):
  user_domain.create(
     kwargs["email "],
     kwargs["role "],
  )
  return AddStakeholderPayload(success=True)
Notes
Note
We use Secure ObjectTypes to enforce authorization. They have the authz attribute that receives a list of authorization classes like: AuthzUserAccess, AuthzGroupAccess, AuthzGroupAttribute, and AuthzOrganizationAccess.

Subscriptions

Integrates GraphQL subscriptions can be found on the 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.

Notes
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,
)

@S UBSCRIPTION.source (" getCustomFix")
def generator( _parentNoneinfo: GraphQLResolveInfo,  AsyncGenerator[str,  None]):
   return "test"

@S UBSCRIPTION.field (" getCustomFix")
def resolve( countstr_info: GraphQLResolveInfo,  **_kwarg: str):
   return count
Idea
Tip
Further reading:
  1. Ariadne docs - subscriptions

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 where the operation could not be completed successfully.

Further reading:
  1. https://spec.graphql.org/June2018/#sec-Errors

Authentication

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

This logic is implemented in the require_login validation.

Authorization

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

Levels and roles

A user can have one role for each of the three levels of authorization:
  1. User
  2. Organization
  3. Group
Each role is associated with a set of permissions.

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

Enforcer

An enforcer is an authorization function that checks if the user can act 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 the user can grant) are within the boundary.

The whole application must use these methods for implementing controls. 

Policy

The general methods for getting the user role, granting permissions, or revoking them are in the policy.

Decorators

For resolvers or mutations that require authorized users, decorate the function with the appropriate decorator from decorators :
  1. @enforce_user_level_auth_async
  2. @enforce_organization_level_auth_async
  3. @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 these 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 decisions 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:

  2. type Car {
      color: String
      @deprecated(
         reason"This field is deprecated and will be removed after 2022/11/13"
      )
    }

  3. Wait until one day after the given deprecation date, and remove the field:

  4. 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 the 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 Color instead of a String:
  1. create a NewColor newColor field that returns the Color type:

  2. type Car {
      color: String  
      newColor: Color
    }

  3. Mark the color field as deprecated and set newColor as the alternative:

  4. 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
    } 

  5. Wait until one day after the given deprecation date and remove the color field:

  6. type Car
      newColor: Color
    }

  7. Add a new color field that uses the Color type:

  8. type Car  
      color: String  
      newColor: Color
    }

  9. Mark the newColor newColor field as deprecated and set color as the alternative:

  10. 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."  
        )
    }
     

  11. Wait until one day after the given deprecation date and remove the newColor field:

  12. type Car  
      color: Color
    }
Notes
Note
These steps may change depending on what you want to do, just keep in mind that keeping backwards compatibility is what really matters.
Idea
Tip
Have an idea to simplify our architecture or noticed docs that could use some love? Don't hesitate to open an issue or submit improvements.