Juniper: Is it possible to add authorization system on Juniper

Created on 31 Oct 2019  路  8Comments  路  Source: graphql-rust/juniper

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

enhancement

Most helpful comment

@davidpdrsn
We didn't understand each other and this is mainly my fault.

  • we want to avoid any side effects on our permissions system, so the authorization check has to be made before the function handling the query.
  • the majority of queries are subjects to some sort of authentication/authorization therefore the public calls are the exception
  • the above reason also implies that the user must define these authorizations no matter what
  • we want a compilation error if authorizations are not defined

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):

  1. #[authorized_public] to specify explicitly that a query is public
  2. #[authorized] to specify that the user must be logged in
  3. #[authorized_with] to specify that the user must be logged in and the permissions required
  4. #[authorized_filter] to specify a function to check the permissions used to filter the query result

If either 1, 2 or 3 is not specified on a query, we throw a compilation error.

All 8 comments

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:

  • Permissions implementation would be checked at compilation time and generate errors / warning if not implemented or not implemented correctly
  • A custom filter could be applied if the return is an iterable type

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.

  • we want to avoid any side effects on our permissions system, so the authorization check has to be made before the function handling the query.
  • the majority of queries are subjects to some sort of authentication/authorization therefore the public calls are the exception
  • the above reason also implies that the user must define these authorizations no matter what
  • we want a compilation error if authorizations are not defined

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):

  1. #[authorized_public] to specify explicitly that a query is public
  2. #[authorized] to specify that the user must be logged in
  3. #[authorized_with] to specify that the user must be logged in and the permissions required
  4. #[authorized_filter] to specify a function to check the permissions used to filter the query result

If 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.

Was this page helpful?
0 / 5 - 0 ratings