Hello,
I have my GraphQL API with Juniper and I want to add permissions to call a graphql_object/to get his result/to get some fields of the result.
With Type-GraphQL we can do something like that :
class MyObject {
@Field()
publicField: string;
@Authorized()
@Field()
authorizedField: string;
@Authorized("ADMIN")
@Field()
adminField: string;
@Authorized(["ADMIN", "MODERATOR"])
@Field({ nullable: true })
hiddenField?: string;
}
In Rust with Juniper I'll like to know if there is a way to do it ? I didn't find anything about it so I think about a good syntax to make it easy to use. There is something similar to this in Juniper ? :
impl Queries {
#[authorized_call("ADMIN")]
#[authorized_return(readPermissions)]
fn user(
context: &Context,
id: Option<String>,
email: Option<String>,
username: Option<String>,
role: Option<String>,
) -> FieldResult<Option<User>> {
Ok(find_user(context, id, email, username, role))
}
}
Thank you for your time
This is not currently possible, but definitely desired.
The async migration will take precedence for now, but this would not be terribly hard to implement.
We could even implement this in a relatively type-safe manner.
Currently your best bet is something more manual, but actually not much more verbose than a built-in solution (apart from #[derive(GraphQLObject)] structs):
enum Permission {
ReadPost,
ReadOwnPost,
WritePost,
...
}
struct Context {
user: Option<User>,
}
impl Context {
fn authorize(&self, permissions: &'static [Permission]) -> Result<(), FieldError> {
// ...
}
}
#[juniper::object(Context = Context)]
impl Query {
fn post(context: &Context, id: u64) -> Result<Post, FieldError> {
context.authorize(&[Permission::ReadPost])?;
...
}
}
@theduke
Hi and thank you for your answer!
We really appreciate your alternative but we would like to have a compilation-time, type-safe permissions system with the following characteristics:
We started using Rust for this kind of usage and would like to use it to its full potential since we're not able to do something like this with other languages.
We also think that having automated permissions documentation would be a great future feature.
As @AurelienFT mentioned it earlier, it would look like this:
impl Query {
#[authorized_call("ADMIN")]
#[filter_result(readPermissions)]
fn user(
context: &Context,
id: Option<String>,
email: Option<String>,
username: Option<String>,
role: Option<String>,
) -> FieldResult<Option<User>> {
Ok(find_user(context, id, email, username, role))
}
}
We really want to implement this in the Juniper crate, can we create a MR to do so?
Also, do you have any possible suggestions on macro syntax?
Thank you for your time!
@EituKo What are you thinking #[authorized_call("ADMIN")] and #[filter_result(readPermissions)] would expand into? Like what code would be generated in the end?
@davidpdrsn
#[authorized_call("ADMIN")] would expand into a function that defines the permissions needed to call the associated query / mutation.
#[filter_result(readPermissions)] would expand into a function that filters the iterable objects returned using a given functor, readPermissions in this case.
Isn鈥檛 that pretty much what @theduke suggested? If you want things to be more type safe I guess you would have to make the return type something like Option<User<Authorized>> where Authorized is in a PhantomData.
@davidpdrsn
We didn't understand each other and this is mainly my fault.
This justifies the use of a proc_macro that expands into a function wrapping the function handling the query. If authorizations are not defined correctly, the wrapped function is not called and a compilation error pops.
These are the macros (names are not definitive):
#[authorized_public] to specify explicitly that a query is public#[authorized] to specify that the user must be logged in#[authorized_with] to specify that the user must be logged in and the permissions required#[authorized_filter] to specify a function to check the permissions used to filter the query resultIf either 1, 2 or 3 is not specified on a query, we throw a compilation error.
Imo if there is a generic, non-Authorization-related, way Juniper can help you accomplish what you want, that would be a great addition to Juniper.
However, building authorization on top of Juniper is a bit of an anti-pattern. See the GraphQL.org's page on best practices for authorization:
https://graphql.org/learn/authorization/
In your example, the recommended practice would be for the find_user function to be responsible for authorization, not the API layer.
Agreed! If Juniper doesn't have the proper hooks, please open an issue on the missing extension / integration point.
Most helpful comment
@davidpdrsn
We didn't understand each other and this is mainly my fault.
This justifies the use of a
proc_macrothat expands into a function wrapping the function handling the query. If authorizations are not defined correctly, the wrapped function is not called and a compilation error pops.These are the macros (names are not definitive):
#[authorized_public]to specify explicitly that a query ispublic#[authorized]to specify that the user must be logged in#[authorized_with]to specify that the user must be logged in and the permissions required#[authorized_filter]to specify a function to check the permissions used to filter the query resultIf either 1, 2 or 3 is not specified on a query, we throw a compilation error.