Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is it possible to add authorization system on Juniper #447

Closed
AurelienFT opened this issue Oct 31, 2019 · 9 comments
Closed

Is it possible to add authorization system on Juniper #447

AurelienFT opened this issue Oct 31, 2019 · 9 comments
Labels
enhancement Improvement of existing features or bugfix

Comments

@AurelienFT
Copy link

AurelienFT commented Oct 31, 2019

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

@theduke
Copy link
Member

theduke commented Oct 31, 2019

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 theduke added the enhancement Improvement of existing features or bugfix label Oct 31, 2019
@ghost
Copy link

ghost commented Nov 8, 2019

@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!

@davidpdrsn
Copy link
Contributor

@EituKo What are you thinking #[authorized_call("ADMIN")] and #[filter_result(readPermissions)] would expand into? Like what code would be generated in the end?

@ghost
Copy link

ghost commented Nov 8, 2019

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

@davidpdrsn
Copy link
Contributor

davidpdrsn commented Nov 8, 2019

Isn’t 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.

@ghost
Copy link

ghost commented Nov 14, 2019

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

@ccbrown
Copy link
Contributor

ccbrown commented Jul 28, 2020

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.

@LegNeato
Copy link
Member

Agreed! If Juniper doesn't have the proper hooks, please open an issue on the missing extension / integration point.

@AlbertMarashi
Copy link

Really would like a way to define directives with attributes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Improvement of existing features or bugfix
Projects
None yet
Development

No branches or pull requests

6 participants