grafbase_sdk/extension/authorization.rs
1use crate::{
2 component::AnyExtension,
3 types::{
4 AuthenticatedRequestContext, AuthorizationDecisions, AuthorizeQueryOutput, AuthorizedOperationContext,
5 Configuration, Error, ErrorResponse, Headers, QueryElements, ResponseElements, SubgraphHeaders,
6 },
7};
8
9/// An authorization extension can grant or deny access to fields, objects, interfaces, unions,
10/// scalars or enums. It's composed of two parts:
11/// - a Wasm module holding the business logic executed by the gateway.
12/// - GraphQL directives, provided to subgraph owners to annotate which elements need
13/// authorization.
14///
15/// Authorization is done in two steps:
16/// - Before starting the execution, [authorize_query()](AuthorizationExtension::authorize_query()) will be called once with all the elements
17/// that must be authorized. Any denied elements will *not* be requested from subgraphs.
18/// - Optionally, if authorization depends on response data, [authorize_response()](AuthorizationExtension::authorize_response()) will be called
19/// to modify the response.
20///
21/// Authorization does not impact the query planning step. Similar to directives like `@include`
22/// and `@skip`, the gateway will modify the query plan rather than the original query. So the
23/// performance impact is fairly minimal.
24///
25/// # Example
26///
27/// You can initialize a new authorization extension with the Grafbase CLI:
28///
29/// ```bash
30/// grafbase extension init --type authorization my-auth
31/// ```
32///
33/// This will generate the following:
34///
35/// ```rust
36/// use grafbase_sdk::{
37/// AuthorizationExtension, IntoAuthorizeQueryOutput,
38/// types::{AuthenticatedRequestContext, SubgraphHeaders, Configuration, ErrorResponse, Error, QueryElements, AuthorizationDecisions}
39/// };
40///
41/// #[derive(AuthorizationExtension)]
42/// struct MyAuth {
43/// config: Config
44/// }
45///
46/// #[derive(serde::Deserialize)]
47/// struct Config {
48/// my_custom_key: String
49/// }
50///
51/// impl AuthorizationExtension for MyAuth {
52/// fn new(config: Configuration) -> Result<Self, Error> {
53/// let config: Config = config.deserialize()?;
54/// Ok(Self { config })
55/// }
56///
57/// fn authorize_query(
58/// &mut self,
59/// ctx: &AuthenticatedRequestContext,
60/// headers: &SubgraphHeaders,
61/// elements: QueryElements<'_>,
62/// ) -> Result<impl IntoAuthorizeQueryOutput, ErrorResponse> {
63/// Ok(AuthorizationDecisions::deny_all("Unauthorized"))
64/// }
65/// }
66/// ```
67///
68/// ## Configuration
69///
70/// The configuration provided in the `new` method is the one defined in the `grafbase.toml`
71/// file by the extension user:
72///
73/// ```toml
74/// [extensions.my-auth.config]
75/// my_custom_key = "value"
76/// ```
77///
78/// Once your business logic is written down you can compile your extension with:
79///
80/// ```bash
81/// grafbase extension build
82/// ```
83///
84/// It will generate all the necessary files in a `build` directory which you can specify in the
85/// `grafbase.toml` configuration with:
86///
87/// ```toml
88/// [extensions.my-auth]
89/// path = "<project path>/build"
90/// ```
91///
92/// ## Directives
93///
94/// In addition to the Rust extension, a `definitions.graphql` file will be also generated. It
95/// should define directives for subgraph owners and any necessary input types, scalars or enum
96/// necessary for those. It is those directives that define the elements that must be granted
97/// access by this extension. The gateway will validate that directives are correctly called. The
98///
99/// The simplest example would be a directive without any arguments:
100///
101/// ```graphql
102/// directive @authorize on FIELD_DEFINITION
103/// ```
104///
105/// Arguments can be static:
106///
107/// ```graphql
108/// directive @authorize(meta: Metadata!) on FIELD_DEFINITION
109///
110/// input Metadata {
111/// key: String!
112/// }
113/// ```
114///
115/// Or they can be dynamically injected from query or response data by the gateway
116/// using one of the scalars defined in the [Grafbase spec](https://specs.grafbase.com/grafbase):
117///
118/// ```graphql
119/// extend schema
120/// @link(
121/// url: "https://specs.grafbase.com/grafbase"
122/// import: ["InputValueSet"]
123/// )
124///
125/// directive @authorize(arguments: InputValueSet) on FIELD_DEFINITION
126/// ```
127///
128#[allow(unused_variables)]
129pub trait AuthorizationExtension: Sized + 'static {
130 /// Creates a new instance of the extension. The [Configuration] will contain all the
131 /// configuration defined in the `grafbase.toml` by the extension user in a serialized format.
132 ///
133 /// # Example
134 ///
135 /// The following TOML configuration:
136 /// ```toml
137 /// [extensions.my-auth.config]
138 /// my_custom_key = "value"
139 /// ```
140 ///
141 /// can be easily deserialized with:
142 ///
143 /// ```rust
144 /// # use grafbase_sdk::types::{Configuration, Error};
145 /// # fn dummy(config: Configuration) -> Result<(), Error> {
146 /// #[derive(serde::Deserialize)]
147 /// struct Config {
148 /// my_custom_key: String
149 /// }
150 ///
151 /// let config: Config = config.deserialize()?;
152 /// # Ok(())
153 /// # }
154 /// ```
155 fn new(config: Configuration) -> Result<Self, Error>;
156
157 /// Authorize query elements before sending any subgraph requests. It is executed after query
158 /// planning and modifies the resulting plan to minimize the performance impact. Any denied
159 /// elements will *not* be requested from subgraphs.
160 ///
161 /// Access control should be returned with [AuthorizationDecisions] which can be constructed in
162 /// multiple ways:
163 /// - [AuthorizationDecisions::grant_all()] will grant access to all elements.
164 /// - [AuthorizationDecisions::deny_all()] will deny access to all elements.
165 /// - [AuthorizationDecisions::deny_some_builder()] creates a builder to deny some of the
166 /// elements. Elements that have not been explicitly denied will be granted access.
167 /// The simplest example being the following:
168 ///
169 /// ```rust
170 /// # use grafbase_sdk::types::{QueryElements, ErrorResponse, AuthorizationDecisions};
171 /// # fn authorize_query(
172 /// # elements: QueryElements<'_>,
173 /// # ) -> Result<AuthorizationDecisions, ErrorResponse> {
174 /// let mut builder = AuthorizationDecisions::deny_some_builder();
175 ///
176 /// for element in elements {
177 /// builder.deny(element, "Unauthorized");
178 /// }
179 ///
180 /// Ok(builder.build())
181 /// # }
182 /// ```
183 ///
184 /// Each [QueryElement](crate::types::QueryElement) will provide:
185 /// - [directive_site](crate::types::QueryElement::directive_site()) providing information on where the directive is applied, field name, etc.
186 /// - [directive_arguments](crate::types::QueryElement::directive_arguments()) which similarly to the configuration
187 /// can be used to deserialize the directive arguments. The underlying format is unspecified,
188 /// but it'll always be a binary format without string escaping so it's safe to use
189 /// `[serde(borrow)] &'a str`.
190 ///
191 /// ```rust
192 /// # use grafbase_sdk::types::{QueryElements, DirectiveSite, ErrorResponse, AuthorizationDecisions};
193 /// # fn authorize_query(
194 /// # elements: QueryElements<'_>,
195 /// # ) -> Result<AuthorizationDecisions, ErrorResponse> {
196 /// let mut builder = AuthorizationDecisions::deny_some_builder();
197 ///
198 /// // For a directive like `@authorize(key: String!)`
199 /// #[derive(serde::Deserialize)]
200 /// struct DirectiveArguments<'a> {
201 /// #[serde(borrow)]
202 /// key: &'a str,
203 /// }
204 ///
205 /// for element in elements {
206 /// match element.directive_site() {
207 /// DirectiveSite::FieldDefinition(field) => {
208 /// field.name();
209 /// }
210 /// _ => return Err(ErrorResponse::internal_server_error()),
211 /// }
212 /// let arguments: DirectiveArguments<'_> = element.directive_arguments()?;
213 /// }
214 ///
215 /// Ok(builder.build())
216 /// # }
217 /// ```
218 ///
219 /// # Example
220 ///
221 /// Supposing the following `defintions.graphql`
222 ///
223 /// ```graphql
224 /// directive @authorize on FIELD_DEFINITION
225 /// ```
226 ///
227 /// With the following subgraph schema:
228 ///
229 /// ```graphql
230 /// type Query {
231 /// user(id: ID!): User @authorize
232 /// }
233 ///
234 /// type User {
235 /// id: ID!
236 /// name: String
237 /// }
238 /// ```
239 ///
240 /// If the client request:
241 /// - `query { user(id: 1) { name } }`: the extension will be called
242 /// with a single [QueryElement](crate::types::QueryElement) with a
243 /// [FieldDefinitionDirectiveSite](crate::types::FieldDefinitionDirectiveSite).
244 /// - `query { a: user(id: 1) b: user(id: 2) }`: the extension will only receive one
245 /// element if no directive argument depend on the field arguments. But if they do, through
246 /// `InputValueSet` for example, then there will be a
247 /// [QueryElement](crate::types::QueryElement) for both `a` and `b`.
248 /// - `query { __typename }`: the extension is not called at all.
249 ///
250 /// Only elements explicitly mentioned in the query will be taken into account:
251 ///
252 /// ```graphql
253 /// type Query {
254 /// node: Node
255 /// }
256 ///
257 /// interface Node {
258 /// id: ID!
259 /// }
260 ///
261 /// type User @authorize implements Node {
262 /// id: ID!
263 /// }
264 /// ```
265 ///
266 /// With a query like `query { node { id } }`, authorization will never be called even if the
267 /// underlying object is a `User`.
268 ///
269 fn authorize_query(
270 &mut self,
271 ctx: &AuthenticatedRequestContext,
272 headers: &SubgraphHeaders,
273 elements: QueryElements<'_>,
274 ) -> Result<impl IntoAuthorizeQueryOutput, ErrorResponse>;
275
276 /// Authorize response elements after receiving data from subgraphs. As of today this function
277 /// will be called as soon as the data is available, so if multiple response elements need
278 /// authorization this method be called multiple times.
279 ///
280 /// This method is meant to be used with [authorize_query()](AuthorizationExtension::authorize_query()).
281 /// Any element that reaches this stage will first pass through query authorization. So it must
282 /// be first granted in the query stage. In addition, directive arguments are split between one
283 /// that depend on response data and those that do not. [authorize_query()](AuthorizationExtension::authorize_query())
284 /// will receive the latter and this function the latter.
285 ///
286 /// So for example with a directive defined as follows:
287 /// ```graphql
288 /// extend schema
289 /// @link(
290 /// url: "https://specs.grafbase.com/grafbase"
291 /// import: ["FieldSet"]
292 /// )
293 ///
294 /// directive @authorized(
295 /// static: String,
296 /// fields: FieldSet
297 /// ) on FIELD_DEFINITION
298 /// ```
299 /// Used in a subgraph schema like:
300 /// ```graphql
301 /// type Query {
302 /// accounts: [Account!]
303 /// }
304 ///
305 /// type Account {
306 /// id: ID!
307 /// owner: User! @authorized(static: "data", fields: "id")
308 /// }
309 ///
310 /// type User {
311 /// id: ID!
312 /// }
313 /// ```
314 /// Then the [authorize_query()](AuthorizationExtension::authorize_query()) method would
315 /// receive `{"static": "data"}` arguments and this method would receive `{"fields": {"id": 1}}`.
316 ///
317 /// That's why this method receives a `state` argument provided by [authorize_query()](AuthorizationExtension::authorize_query()).
318 fn authorize_response(
319 &mut self,
320 ctx: &AuthorizedOperationContext,
321 state: Vec<u8>,
322 elements: ResponseElements<'_>,
323 ) -> Result<AuthorizationDecisions, Error> {
324 Err("Response authorization not implemented".into())
325 }
326}
327
328pub trait IntoAuthorizeQueryOutput {
329 fn into_authorize_query_output(self) -> AuthorizeQueryOutput;
330}
331
332impl IntoAuthorizeQueryOutput for AuthorizeQueryOutput {
333 fn into_authorize_query_output(self) -> AuthorizeQueryOutput {
334 self
335 }
336}
337
338impl IntoAuthorizeQueryOutput for AuthorizationDecisions {
339 fn into_authorize_query_output(self) -> AuthorizeQueryOutput {
340 AuthorizeQueryOutput::new(self)
341 }
342}
343
344impl IntoAuthorizeQueryOutput for (Headers, AuthorizationDecisions) {
345 fn into_authorize_query_output(self) -> AuthorizeQueryOutput {
346 let (headers, decisions) = self;
347 AuthorizeQueryOutput::new(decisions).headers(headers)
348 }
349}
350
351impl IntoAuthorizeQueryOutput for (AuthorizationDecisions, Headers) {
352 fn into_authorize_query_output(self) -> AuthorizeQueryOutput {
353 let (decisions, headers) = self;
354 AuthorizeQueryOutput::new(decisions).headers(headers)
355 }
356}
357
358#[doc(hidden)]
359pub fn register<T: AuthorizationExtension>() {
360 pub(super) struct Proxy<T: AuthorizationExtension>(T);
361
362 impl<T: AuthorizationExtension> AnyExtension for Proxy<T> {
363 fn authorize_query(
364 &mut self,
365 ctx: &AuthenticatedRequestContext,
366 headers: &SubgraphHeaders,
367 elements: QueryElements<'_>,
368 ) -> Result<AuthorizeQueryOutput, ErrorResponse> {
369 self.0
370 .authorize_query(ctx, headers, elements)
371 .map(|output| output.into_authorize_query_output())
372 }
373
374 fn authorize_response(
375 &mut self,
376 ctx: &AuthorizedOperationContext,
377 state: Vec<u8>,
378 elements: ResponseElements<'_>,
379 ) -> Result<AuthorizationDecisions, Error> {
380 self.0.authorize_response(ctx, state, elements)
381 }
382 }
383
384 crate::component::register_extension(Box::new(|_, config| {
385 <T as AuthorizationExtension>::new(config).map(|extension| Box::new(Proxy(extension)) as Box<dyn AnyExtension>)
386 }))
387}