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}