pub trait AuthorizationExtension: Sized + 'static {
// Required methods
fn new(config: Configuration) -> Result<Self, Error>;
fn authorize_query(
&mut self,
ctx: &AuthenticatedRequestContext,
headers: &SubgraphHeaders,
elements: QueryElements<'_>,
) -> Result<impl IntoAuthorizeQueryOutput, ErrorResponse>;
// Provided method
fn authorize_response(
&mut self,
ctx: &AuthorizedOperationContext,
state: Vec<u8>,
elements: ResponseElements<'_>,
) -> Result<AuthorizationDecisions, Error> { ... }
}
Expand description
An authorization extension can grant or deny access to fields, objects, interfaces, unions, scalars or enums. It’s composed of two parts:
- a Wasm module holding the business logic executed by the gateway.
- GraphQL directives, provided to subgraph owners to annotate which elements need authorization.
Authorization is done in two steps:
- Before starting the execution, authorize_query() will be called once with all the elements that must be authorized. Any denied elements will not be requested from subgraphs.
- Optionally, if authorization depends on response data, authorize_response() will be called to modify the response.
Authorization does not impact the query planning step. Similar to directives like @include
and @skip
, the gateway will modify the query plan rather than the original query. So the
performance impact is fairly minimal.
§Example
You can initialize a new authorization extension with the Grafbase CLI:
grafbase extension init --type authorization my-auth
This will generate the following:
use grafbase_sdk::{
AuthorizationExtension, IntoAuthorizeQueryOutput,
types::{AuthenticatedRequestContext, SubgraphHeaders, Configuration, ErrorResponse, Error, QueryElements, AuthorizationDecisions}
};
#[derive(AuthorizationExtension)]
struct MyAuth {
config: Config
}
#[derive(serde::Deserialize)]
struct Config {
my_custom_key: String
}
impl AuthorizationExtension for MyAuth {
fn new(config: Configuration) -> Result<Self, Error> {
let config: Config = config.deserialize()?;
Ok(Self { config })
}
fn authorize_query(
&mut self,
ctx: &AuthenticatedRequestContext,
headers: &SubgraphHeaders,
elements: QueryElements<'_>,
) -> Result<impl IntoAuthorizeQueryOutput, ErrorResponse> {
Ok(AuthorizationDecisions::deny_all("Unauthorized"))
}
}
§Configuration
The configuration provided in the new
method is the one defined in the grafbase.toml
file by the extension user:
[extensions.my-auth.config]
my_custom_key = "value"
Once your business logic is written down you can compile your extension with:
grafbase extension build
It will generate all the necessary files in a build
directory which you can specify in the
grafbase.toml
configuration with:
[extensions.my-auth]
path = "<project path>/build"
§Directives
In addition to the Rust extension, a definitions.graphql
file will be also generated. It
should define directives for subgraph owners and any necessary input types, scalars or enum
necessary for those. It is those directives that define the elements that must be granted
access by this extension. The gateway will validate that directives are correctly called. The
The simplest example would be a directive without any arguments:
directive @authorize on FIELD_DEFINITION
Arguments can be static:
directive @authorize(meta: Metadata!) on FIELD_DEFINITION
input Metadata {
key: String!
}
Or they can be dynamically injected from query or response data by the gateway using one of the scalars defined in the Grafbase spec:
extend schema
@link(
url: "https://specs.grafbase.com/grafbase"
import: ["InputValueSet"]
)
directive @authorize(arguments: InputValueSet) on FIELD_DEFINITION
Required Methods§
Sourcefn new(config: Configuration) -> Result<Self, Error>
fn new(config: Configuration) -> Result<Self, Error>
Creates a new instance of the extension. The Configuration will contain all the
configuration defined in the grafbase.toml
by the extension user in a serialized format.
§Example
The following TOML configuration:
[extensions.my-auth.config]
my_custom_key = "value"
can be easily deserialized with:
#[derive(serde::Deserialize)]
struct Config {
my_custom_key: String
}
let config: Config = config.deserialize()?;
Authorize query elements before sending any subgraph requests. It is executed after query planning and modifies the resulting plan to minimize the performance impact. Any denied elements will not be requested from subgraphs.
Access control should be returned with AuthorizationDecisions which can be constructed in multiple ways:
- AuthorizationDecisions::grant_all() will grant access to all elements.
- AuthorizationDecisions::deny_all() will deny access to all elements.
- AuthorizationDecisions::deny_some_builder() creates a builder to deny some of the elements. Elements that have not been explicitly denied will be granted access. The simplest example being the following:
let mut builder = AuthorizationDecisions::deny_some_builder();
for element in elements {
builder.deny(element, "Unauthorized");
}
Ok(builder.build())
Each QueryElement will provide:
- directive_site providing information on where the directive is applied, field name, etc.
- directive_arguments which similarly to the configuration
can be used to deserialize the directive arguments. The underlying format is unspecified,
but it’ll always be a binary format without string escaping so it’s safe to use
[serde(borrow)] &'a str
.
let mut builder = AuthorizationDecisions::deny_some_builder();
// For a directive like `@authorize(key: String!)`
#[derive(serde::Deserialize)]
struct DirectiveArguments<'a> {
#[serde(borrow)]
key: &'a str,
}
for element in elements {
match element.directive_site() {
DirectiveSite::FieldDefinition(field) => {
field.name();
}
_ => return Err(ErrorResponse::internal_server_error()),
}
let arguments: DirectiveArguments<'_> = element.directive_arguments()?;
}
Ok(builder.build())
§Example
Supposing the following defintions.graphql
directive @authorize on FIELD_DEFINITION
With the following subgraph schema:
type Query {
user(id: ID!): User @authorize
}
type User {
id: ID!
name: String
}
If the client request:
query { user(id: 1) { name } }
: the extension will be called with a single QueryElement with a FieldDefinitionDirectiveSite.query { a: user(id: 1) b: user(id: 2) }
: the extension will only receive one element if no directive argument depend on the field arguments. But if they do, throughInputValueSet
for example, then there will be a QueryElement for botha
andb
.query { __typename }
: the extension is not called at all.
Only elements explicitly mentioned in the query will be taken into account:
type Query {
node: Node
}
interface Node {
id: ID!
}
type User @authorize implements Node {
id: ID!
}
With a query like query { node { id } }
, authorization will never be called even if the
underlying object is a User
.
Provided Methods§
Authorize response elements after receiving data from subgraphs. As of today this function will be called as soon as the data is available, so if multiple response elements need authorization this method be called multiple times.
This method is meant to be used with authorize_query(). Any element that reaches this stage will first pass through query authorization. So it must be first granted in the query stage. In addition, directive arguments are split between one that depend on response data and those that do not. authorize_query() will receive the latter and this function the latter.
So for example with a directive defined as follows:
extend schema
@link(
url: "https://specs.grafbase.com/grafbase"
import: ["FieldSet"]
)
directive @authorized(
static: String,
fields: FieldSet
) on FIELD_DEFINITION
Used in a subgraph schema like:
type Query {
accounts: [Account!]
}
type Account {
id: ID!
owner: User! @authorized(static: "data", fields: "id")
}
type User {
id: ID!
}
Then the authorize_query() method would
receive {"static": "data"}
arguments and this method would receive {"fields": {"id": 1}}
.
That’s why this method receives a state
argument provided by authorize_query().
Dyn Compatibility§
This trait is not dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety", so this trait is not object safe.