$30 off During Our Annual Pro Sale. View Details »

Authorization in the GraphQL era

Authorization in the GraphQL era

Rails Conf 2020

More and more teams choose GraphQL as the transport protocol for their projects. It makes sense because GraphQL has several features that are missing in the standard REST API. At the same time, there is a lot of information about the shortcomings of the GraphQL, such as the N+1 problem, and developers from the very beginning keep them in mind. However, few people think about another feature of GraphQL - the access control organization. Are there any differences between graphs nodes and actions in controllers when working with permissions? Spoiler: yes, and in my speech, I want to tell you about it and what options we have in the Rails ecosystem.

Nikolay Sverchkov

May 03, 2020
Tweet

More Decks by Nikolay Sverchkov

Other Decks in Programming

Transcript

  1. Authorization 90 % GraphQL 10 % Buzzwords 
 of 


    GraphQL 100 % How do I see my talk What do people expect
  2. Standard Rails App DB UsersController PostsController GET /users POST /users

    ImagesController ... GET /posts/:id PUT /posts/:id ... GET /images POST /images ... ... BUSINESS LOGIC LAYER
  3. Standard Rails App DB UsersController PostsController GET /users POST /users

    ImagesController ... GET /posts/:id PUT /posts/:id ... GET /images POST /images ... ... BUSINESS LOGIC LAYER User
  4. Standard Rails App DB UsersController PostsController GET /users POST /users

    ImagesController ... GET /posts/:id PUT /posts/:id ... GET /images POST /images ... ... BUSINESS LOGIC LAYER Data
  5. Standard Rails App DB UsersController PostsController GET /users POST /users

    ImagesController ... GET /posts/:id PUT /posts/:id ... GET /images POST /images ... ... BUSINESS LOGIC LAYER RESTFul
  6. Standard* Rails App DB UsersController PostsController GET /users POST /users

    ImagesController ... GET /posts/:id PUT /posts/:id ... GET /images POST /images ... ... BUSINESS LOGIC LAYER *ACCESS CONTROL
 LAYER RESTFul
  7. REST\RESTfull is a software architectural style that defines a set

    of constraints to be used for creating web services (c) Wikipedia
  8. GraphQL is a query language for APIs and a runtime

    for fulfilling those queries with your existing data.
  9. GraphqlControll#execute class GraphqlControll < ApplicationController!::Base def execute query = params[:query]

    result = AppSchema.execute(query, params: params) render json: result end end
  10. AppSchema#execute(q, p) GraphQL Internals Types-- Mutations-- POST /graphql TagType UserType

    PostType ImageType ProfileType CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController
  11. AppSchema#execute(q, p) GraphQL Internals Types-- Mutations-- POST /graphql TagType UserType

    PostType ImageType ProfileType CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController
  12. AppSchema#execute(q, p) GraphQL Internals Types-- Mutations-- POST /graphql TagType UserType

    PostType ImageType ProfileType CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController
  13. AppSchema#execute(q, p) GraphQL Internals Types-- Mutations-- POST /graphql TagType UserType

    PostType ImageType ProfileType CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController
  14. Authentication Auth Framework User Model Crypto Layer Helpers (e.g. emails)

    ... (e.g. devise) GraphQL RESTfull ? How to use Framework ?
  15. AppSchema#execute Login\Logout? Types-- Mutations-- POST /graphql TagType UserType PostType ImageType

    ProfileType CommentType LoginUser LogoutUser UpdateProfileMutation GraphqlController
  16. AppSchema#execute Login\Logout? Types-- Mutations-- POST /graphql TagType UserType PostType ImageType

    ProfileType CommentType LoginUser LogoutUser UpdateProfileMutation GraphqlController # NOT NECESSARY
  17. AppSchema#execute Login\Logout? Types-- Mutations-- POST /graphql TagType UserType PostType ImageType

    ProfileType CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController GOOD [DeviseControllers]
  18. class GraphqlControll < ApplicationController!::Base def execute query = params[:query] context

    = {current_user: get_user_from_api_token} result = AppSchema.execute(query, context: context) render json: result end end GraphqlControll#execute
  19. UsersController PostsController POST /users ImagesController ... GET /posts/:id PUT /posts/:id

    ... GET /images POST /images ... *ACCESS CONTROL
 LAYER GET /users Ensuring Authentication
  20. before_action :authenticate_user! UsersController PostsController POST /users ImagesController ... GET /posts/:id

    PUT /posts/:id ... GET /images POST /images ... *ACCESS CONTROL
 LAYER GET /users
  21. AppSchema#execute The One Entrypoint Types-- Mutations-- POST /graphql TagType UserType

    PostType ImageType ProfileType CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController
  22. AppSchema#execute The One Entrypoint Types-- Mutations-- POST /graphql TagType UserType

    PostType ImageType ProfileType CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController
  23. class GraphqlControll < ApplicationController!::Base def execute query = params[:query] context

    = {current_user: get_user_from_api_token} result = AppSchema.execute(query, context: context) render json: result end end GraphqlControll#execute * context - state passed through the entire request
  24. AppSchema#execute(q, p) GraphQL Internals Types-- Mutations-- POST /graphql TagType UserType

    PostType ImageType ProfileType CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController
  25. class UserType < BaseType field :profile, ProfileType def profile user.profile

    end end UserType !-> ProfileType class ProfileType < BaseType field :full_name !!... end
  26. class UserType < BaseType field :profile, ProfileType def profile raise

    NotAuthorized unless context[:current_user]&.is_a?(User) user.profile end end UserType !-> ProfileType
  27. class UserType < BaseType field :profile, ProfileType def profile raise

    NotAuthorized unless context[:current_user]&.is_a?(User) user.profile end end UserType !-> ProfileType Maintainability
  28. class UserType < BaseType field :profile, ProfileType def profile user.profile

    end def visible?(context) raise NotAuthorized unless context[:current_user]&.is_a?(User) end end Visibility * visibility - special graphql-ruby feature
  29. module Extensions module Visibility def visible?(context) return context[:current_user]&.is_a?(User) super end

    end end BaseType.prepend(Extensions!::Visibility) Visibility Extension Global
  30. Visibility Extension module Extensions module Visibility def initialize(*args, login_required: false,

    !**kwargs, &block) super(*args, !**kwargs, &block) @login_required = login_required end def visible?(context) return context[:current_user]&.is_a?(User) if @login_required super end end end
  31. Why Is Visibility Better? CommentTyp # GraphQL query { !__schema

    { types { name } } } GraphQL Introspection
  32. AppSchema#execute Types-- Mutations-- POST /graphql TagType UserType PostType ImageType ProfileType

    CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController Hide Parts of Your Schema
  33. class UserType < BaseType field :posts, PostType.connection_type with_options login_required: true,

    admin: true do field :profile, ProfileType end end And Again Visibility?
  34. class UserType < BaseType def self.authorized?(object, context) (current_user has role)

    !|| (current_user has permission) !|| (business rule) !|| end end GraphQL Authorization * authorized? - graphql-ruby feature
  35. Authorization Auth Library API Abilities Layer Helpers (e.g. for templates)

    ... (e.g. cancan or pundit) GraphQL RESTfull ? How to use Library ?
  36. Ability For Each Action UsersController PostsController POST /users ImagesController ...

    GET /posts/:id PUT /posts/:id ... GET /images POST /images ... *ACCESS CONTROL
 LAYER GET /users can? :read, Image can? :create, Image can? :read, @post can? :update, @post !!...
  37. Ability For Each Action UsersController PostsController POST /users ImagesController ...

    GET /posts/:id PUT /posts/:id ... GET /images POST /images ... *ACCESS CONTROL
 LAYER GET /users can? :read, Image can? :create, Image can? :read, @post can? :update, @post !!... CONTROLLER RESOURCE
  38. AppSchema#execute Types-- Mutations-- POST /graphql TagType UserType PostType ImageType ProfileType

    CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController Ability For Each Node
  39. AppSchema#execute Types-- Mutations-- POST /graphql TagType UserType PostType ImageType ProfileType

    CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController Ability For Each Node author (String) body (String)
  40. 1+ Endpoints for Sets of Fields PostsController GET /posts/:id PUT

    /posts/:id ... *ACCESS CONTROL
 LAYER Admin::PostsController GET /admin/posts/:id PUT /admin/posts/:id ... Post id title body published_at
  41. Yes, but ... • How to pass the current_user? •

    How to handle unauthorized error? • How to ensure authorization happens? • Hot to get rid of boilerplate? ... class UserType < BaseType field :profile, ProfileType def profile profile = user.profile can :read?, profile profile end end
  42. class UserType < BaseType field :profile, ProfileType def profile profile

    = user.profile can :read?, profile profile end end Yes, but ... • How to pass the current_user? • How to handle unauthorized error? • How to ensure authorization happens? • Hot to get rid of boilerplate? ... Library Helpers
  43. class UserType < BaseType field :profile, ProfileType, authorize: true end

    class ProfilePolicy < ApplicationPolicy def show? current_user.id !== record.id end end As Easy as Possible
  44. class UserType < BaseType field :profile, ProfileType, authorize: true end

    class ProfilePolicy < ApplicationPolicy def show? current_user.id !== record.id end end As Easy as Possible
  45. # in your schema file rescue_from(ActionPolicy!::Unauthorized) do |exp| raise GraphQL!::ExecutionError.new(

    exp.result.message, extensions: { code: :unauthorized, fullMessages: exp.result.reasons.full_messages, details: exp.result.reasons.details } ) end Handling Exceptions
  46. AppSchema#execute Types-- Mutations-- POST /graphql TagType UserType PostType ImageType ProfileType

    CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController Scoping Data admin? manager? owner? guest?
  47. class UserType < BaseType field :posts, PostType.connection_type, authorized_scope: true end

    class PostPolicy < ApplicationPolicy # rel !== user.posts relation_scope do |rel| next rel.with_deleted if current_user.admin? next rel if user !== current_user rel.where(published: true) end end Scoping Data
  48. Where is the best place for UX element display logic?

    On Backend On Frontend On Both sides UI/UX 1. 2. 3.
  49. On Backend On Frontend On Both sides UI/UX Expose Where

    is the best place for UX element display logic?
  50. UI/UX { post(id: $id) { canEdit { value # bool

    message # top-level decline message reasons { # detailed information details fullMessage } } !!... }
  51. AppSchema#execute Types-- Mutations-- POST /graphql TagType UserType PostType ImageType ProfileType

    CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController Hot Abilities can :edit?
  52. class UserType < BaseType field :profile, ProfileType, authorize: true end

    class ProfilePolicy < ApplicationPolicy cache :show? def show? # heavy calculation end end Let's Cache it!
  53. AppSchema#execute GraphQL Internals Types-- Mutations-- POST /graphql TagType UserType PostType

    ImageType ProfileType CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController
  54. class CreatePostMutation < BaseMutation !!... def resolve(post:, *attrs) authorize! post,

    to: :create? # business logic here end end Mutations !!<=> Service Objects
  55. class CreatePostMutation < BaseMutation !!... def resolve(post:, *attrs) authorize! post,

    to: :create? # business logic here end end Mutations !!<=> Service Objects # after_action :verify_authorized How to ensure policies are used?
  56. Ensuring Policies are Used class BaseMutation < GraphQL!::Schema!::RelayClassicMutation after_resolve do

    raise "Unauthorized mutation" unless @authorization_performed end def authorize!(*) @authorization_performed = true super end def skip_authorization! @authorization_performed = true end end *Full gist: https://bit.ly/2UYZpSX
  57. Ensuring Policies are Used class BaseMutation < GraphQL!::Schema!::RelayClassicMutation after_resolve do

    raise "Unauthorized mutation" unless @authorization_performed end def authorize!(*) @authorization_performed = true super end def skip_authorization! @authorization_performed = true end end *Full gist: https://bit.ly/2UYZpSX
  58. Only Integration Tests describe PostType do let(:query) do !<<~GRAPHQL query($id:

    ID!){ node(id: $id) { !!... } } GRAPHQL end let(:context) { current_user: create(:user) } it "checks execution" do result = AppSchema.execute(query, context: context, !!...) expect(result["data"]["node"][!!...]).to eq(!!...) end end
  59. Only Integration Tests describe PostType do let(:query) do !<<~GRAPHQL query($id:

    ID!){ node(id: $id) { !!... } } GRAPHQL end let(:context) { current_user: create(:user) } it "checks execution" do result = AppSchema.execute(query, context: context, !!...) expect(result["data"]["node"][!!...]).to eq(!!...) end end
  60. Auth Tests describe PostType do describe "#somefield" do context "when

    user is admin" do let(:current_user) { create(:admin) } # it !!... end context "when user is owner" do let(:current_user) { create(:user, post: post) } # it !!... end context "when user is guest" do let(:current_user) { nil } # it !!... end end end
  61. Bad Auth Tests Testing is not what they should Slow

    Complex describe PostType do describe "#somefield" do context "when user is admin" do let(:current_user) { create(:admin) } # it !!... end context "when user is owner" do let(:current_user) { create(:user, post: post) } # it !!... end context "when user is guest" do let(:current_user) { nil } # it !!... end end end
  62. Good Auth Tests describe PostType do let(:post) { create(:post) }

    subject { # call AppSchema.execute(id: post.id) here } describe "#somefield" do it "is authorized" do expect { subject }.to be_authorized_to(:show?, post) .with(PostPolicy) end end end