Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Ee8543e8f5554aaeabb6d140fa07b919?s=128

Nikolay Sverchkov

May 03, 2020
Tweet

Transcript

  1. AUTH IN THE GraphQL ERA

  2. None
  3. Don't talk with me, I'm a picture!

  4. None
  5. None
  6. evilmartians.com

  7. TATARSTAN

  8. Authorization 90 % GraphQL 10 % Buzzwords 
 of 


    GraphQL 100 % How do I see my talk What do people expect
  9. https://book.productionreadygraphql.com/

  10. GraphQL Auth through the prism of RESTful Auth

  11. Standard Rails App DB UsersController PostsController GET /users POST /users

    ImagesController ... GET /posts/:id PUT /posts/:id ... GET /images POST /images ... ... BUSINESS LOGIC LAYER
  12. 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
  13. 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
  14. 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
  15. Rails DHH way it's me

  16. 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
  17. REST\RESTfull is a software architectural style that defines a set

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

    for fulfilling those queries with your existing data.
  19. DB GraphqlController POST /graphql BUSINESS LOGIC LAYER GraphQL Rails App

    GRAPHQL INTERNALS
  20. DB GraphqlController POST /graphql BUSINESS LOGIC LAYER GraphQL Rails App

    GRAPHQL INTERNALS User Data
  21. DB GraphqlController POST /graphql BUSINESS LOGIC LAYER GraphQL Rails App

    GRAPHQL INTERNALS ???
  22. GraphqlControll#execute class GraphqlControll < ApplicationController!::Base def execute query = params[:query]

    result = AppSchema.execute(query, params: params) render json: result end end
  23. GraphQL Internals

  24. AppSchema#execute(q, p) GraphQL Internals GraphqlController POST /graphql

  25. AppSchema#execute(q, p) Types-- Mutations-- GraphQL Internals GraphqlController POST /graphql *R**

    C*UD
  26. AppSchema#execute(q, p) GraphQL Internals Types-- Mutations-- POST /graphql TagType UserType

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

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

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

    PostType ImageType ProfileType CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController
  30. When You Read First Pages of GraphQL Documentation

  31. *ACCESS CONTROL
 LAYER DB GraphqlController POST /graphql BUSINESS LOGIC LAYER

    GraphQL* Rails App GRAPHQL ENGINE
  32. Authentication before authorization

  33. Authentication WHO IS WHO? Homer#0 Homer#1 Homer#2 Homer#3

  34. Authentication Auth Framework User Model Crypto Layer Helpers (e.g. emails)

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

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

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

    ProfileType CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController GOOD [DeviseControllers]
  38. 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
  39. UsersController PostsController POST /users ImagesController ... GET /posts/:id PUT /posts/:id

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

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

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

    PostType ImageType ProfileType CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController
  43. Welcome graphql-ruby! ⭐ 4.1k on Github

  44. 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
  45. AppSchema#execute(q, p) GraphQL Internals Types-- Mutations-- POST /graphql TagType UserType

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

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

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

    NotAuthorized unless context[:current_user]&.is_a?(User) user.profile end end UserType !-> ProfileType Maintainability
  49. We Need Some Magic!

  50. 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
  51. module Extensions module Visibility def visible?(context) return context[:current_user]&.is_a?(User) super end

    end end BaseType.prepend(Extensions!::Visibility) Visibility Extension
  52. 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
  53. 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
  54. class UserType < BaseType field :posts, PostType.connection_type with_options login_required: true

    do field :profile, ProfileType end end Visibility Extension #
  55. CommentType Why Is Visibility Better Then Other Ways?

  56. Why Is Visibility Better? CommentTyp # GraphQL query { !__schema

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

    CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController Hide Parts of Your Schema
  58. Authorization

  59. Authorization CAN this HOMER HAVE A BEER?

  60. Authorization CAN this HOMER HAVE A BEER?

  61. class UserType < BaseType field :posts, PostType.connection_type with_options login_required: true,

    admin: true do field :profile, ProfileType end end And Again Visibility?
  62. Authorization is Flexible (current_user has role) !|| (current_user has permission)

    !|| (business rule) !|| !!...
  63. 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
  64. Authorization Auth Library API Abilities Layer Helpers (e.g. for templates)

    ... (e.g. cancan or pundit)
  65. Authorization Auth Library API Abilities Layer Helpers (e.g. for templates)

    ... (e.g. cancan or pundit) GraphQL RESTfull ? How to use Library ?
  66. 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 !!...
  67. 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
  68. AppSchema#execute Types-- Mutations-- POST /graphql TagType UserType PostType ImageType ProfileType

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

    CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController Ability For Each Node author (String) body (String)
  70. 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
  71. Can We (re)Use Existing Abilities Layer?

  72. 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
  73. 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
  74. GraphQL Pro

  75. Who Has Heard about gem "action_policy"?

  76. gem "action_policy" The Authorization Framework

  77. gem "action_policy" + GraphQL !== gem "action_policy-graphql"

  78. 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
  79. 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
  80. # 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
  81. # All Docs GraphQL Features

  82. AppSchema#execute Types-- Mutations-- POST /graphql TagType UserType PostType ImageType ProfileType

    CommentType CreatePostMutation AddCommentMutation UpdateProfileMutation GraphqlController Scoping Data admin? manager? owner? guest?
  83. 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
  84. UI/UX CAN HOMER PRESS THE BUTTON?

  85. Where is the best place for UX element display logic?

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

    is the best place for UX element display logic?
  87. class PostType < BaseType expose_authorization_rules :edit?, :destroy? end UI/UX

  88. UI/UX { post(id: $id) { canEdit { value # bool

    message # top-level decline message reasons { # detailed information details fullMessage } } !!... }
  89. Hot Abilities

  90. AppSchema#execute Types-- Mutations-- POST /graphql TagType UserType PostType ImageType ProfileType

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

    class ProfilePolicy < ApplicationPolicy cache :show? def show? # heavy calculation end end Let's Cache it!
  92. Why We Are Talking Only About Reading Data?

  93. AppSchema#execute GraphQL Internals Types-- Mutations-- POST /graphql TagType UserType PostType

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

    to: :create? # business logic here end end Mutations !!<=> Service Objects
  95. 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?
  96. 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
  97. 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
  98. Testing

  99. 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
  100. 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
  101. 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
  102. 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
  103. 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
  104. Conclusion

  105. Conclusion Shared Auth* Libraries Features Features RESTful GraphQL

  106. Conclusion Shared Auth* Libraries Features Features RESTful GraphQL / /

    / / / / /
  107. Conclusion Shared Auth* Libraries Features Features RESTful GraphQL / /

    / / / / /
  108. @ssnickolay Nikolay Sverchkov @ssnickolay THANK YOU! @evilmartians http://evl.ms/blog http://evl.ms/telegram