Skip to main content

Access control

To implement access control, define the following function in the designated src/server-extension/check module:

import {
RequestCheckContext
} from '../../node_modules/@subsquid/graphql-server/src/check'

export async function requestCheck(
req: RequestCheckContext
): Promise<boolean | string> {
...
}

Once defined, this function will be called every time a request arrives. Then,

  • if the function returns true, the request is processed as usual;
  • if the function returns false, the server responds with '{"errors":[{"message":"not allowed"}]}';
  • if the function returns an errorString, the server responds with `{{"errors":[{"message":"${errorString}"}]}`.

The request information such as HTTP headers and GraphQL selections is available in the context. This makes it possible to authenticate the user that sent the query and either allow or deny access. The decision may take the query contents into account, allowing for some authorization granularity.

RequestCheckContext

The context type has the following interface:

RequestCheckContext {
http: {uri: string, method: string, headers: HttpHeaders}
operation: OperationDefinitionNode
operationName: string | null
schema: GraphQLSchema
context: Record<string, any>
model: Model
}

Here,

  • http field contains the low level HTTP info. Information on headers is stored in a Map from lowercase header names to values. For example, req.http.headers.get('authorization') is the value of the authorization header.
  • operation is the root OperationDefinitionNode of the tree describing the query. Useful if the authorization decision depends on the query contents.
  • operationName is the query name.
  • schema is a GraphQLSchema object.
  • context holds a PoolOpenreaderContext at context.openreader. It can be used to access the database, though this is highly discouraged: the interfaces involved are considered to be internal and are subject to change without notice.
  • model is an Openreader data Model.

Sending user data to resolvers

Authentication data such as user name can be passed from requestCheck() to a custom resolver through Openreader context:

export async function requestCheck(req: RequestCheckContext): Promise<boolean | string> {
...
// obtain user name e.g. by decoding the authentication header
let user = ...
// save user name to Openreader context
req.context.openreader.user = user
...
}

A custom resolver that retrieves it may look like this:

@Resolver()
export class UserCommentResolver {
constructor(private tx: () => Promise<EntityManager>) {}

@Query(() => [UserCommentCountQueryResult])
async countUserComments(
@Ctx() ctx: any
): Promise<UserCommentCountQueryResult[]> {
let user = ctx.openreader.user
let manager = await this.tx()
let result: UserCommentCountQueryResult[] =
await manager
.getRepository(UserComment)
.query(`
SELECT COUNT(*) as total
FROM user_comment
WHERE "user" = '${user}'
`)
return result
}

@Mutation(() => Boolean)
async addComment(
@Arg('text') comment: string,
@Ctx() ctx: any
): Promise<Boolean> {
let user = ctx.openreader.user
let manager = await this.tx()
await manager.save(new UserComment({
id: `${user}-${comment}`,
user,
comment
}))
return true
}
}

See full code in this branch.

This approach does not work with subscriptions.

Examples

A simple strategy that authorizes anyone with a 12345 token to perform any query can be implemented with

src/server-extension/check.ts
import {
RequestCheckContext
} from '../../node_modules/@subsquid/graphql-server/src/check'

export async function requestCheck(
req: RequestCheckContext
): Promise<boolean | string> {
return req.http.headers.get('authorization')==='Bearer 12345'
}

A more elaborate example with two users authorized to perform different query sets is available in this repo. Another great example of using requestCheck() for authorization can be spotted in the wild in the code of a squid used by Reef.