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 aMap
from lowercase header names to values. For example,req.http.headers.get('authorization')
is the value of the authorization header.operation
is the rootOperationDefinitionNode
of the tree describing the query. Useful if the authorization decision depends on the query contents.operationName
is the query name.schema
is aGraphQLSchema
object.context
holds aPoolOpenreaderContext
atcontext.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 dataModel
.
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
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.