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.

Nikolay Sverchkov

May 03, 2020
Tweet

More Decks by Nikolay Sverchkov

Other Decks in Programming

Transcript

  1. AUTH
    IN
    THE
    GraphQL
    ERA

    View full-size slide

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

    View full-size slide

  3. evilmartians.com

    View full-size slide

  4. Authorization
    90 %
    GraphQL
    10 %
    Buzzwords 

    of 

    GraphQL
    100 %
    How do I see my talk What do people expect

    View full-size slide

  5. https://book.productionreadygraphql.com/

    View full-size slide

  6. GraphQL Auth
    through the prism of
    RESTful Auth

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  11. Rails DHH way
    it's me

    View full-size slide

  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
    *ACCESS
    CONTROL

    LAYER
    RESTFul

    View full-size slide

  13. REST\RESTfull is a
    software architectural style
    that defines a set of constraints to be used
    for creating web services
    (c) Wikipedia

    View full-size slide

  14. GraphQL is a query language for APIs
    and a runtime for fulfilling those
    queries with your existing data.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  18. GraphqlControll#execute
    class GraphqlControll < ApplicationController!::Base
    def execute
    query = params[:query]
    result = AppSchema.execute(query, params: params)
    render json: result
    end
    end

    View full-size slide

  19. GraphQL Internals

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  26. When You Read
    First Pages of
    GraphQL
    Documentation

    View full-size slide

  27. *ACCESS
    CONTROL

    LAYER
    DB
    GraphqlController
    POST /graphql
    BUSINESS LOGIC LAYER
    GraphQL* Rails App
    GRAPHQL ENGINE

    View full-size slide

  28. Authentication before
    authorization

    View full-size slide

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

    View full-size slide

  30. Authentication
    Auth Framework
    User Model
    Crypto Layer
    Helpers (e.g. emails)
    ...
    (e.g. devise)
    GraphQL
    RESTfull
    ? How to use Framework ?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  33. AppSchema#execute
    Login\Logout?
    Types--
    Mutations--
    POST /graphql
    TagType
    UserType
    PostType ImageType
    ProfileType
    CommentType
    CreatePostMutation
    AddCommentMutation
    UpdateProfileMutation
    GraphqlController
    GOOD
    [DeviseControllers]

    View full-size slide

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

    View full-size slide

  35. UsersController
    PostsController
    POST /users
    ImagesController
    ...
    GET /posts/:id
    PUT /posts/:id
    ...
    GET /images
    POST /images
    ...
    *ACCESS
    CONTROL

    LAYER
    GET /users
    Ensuring Authentication

    View full-size slide

  36. before_action :authenticate_user!
    UsersController
    PostsController
    POST /users
    ImagesController
    ...
    GET /posts/:id
    PUT /posts/:id
    ...
    GET /images
    POST /images
    ...
    *ACCESS
    CONTROL

    LAYER
    GET /users

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  39. Welcome graphql-ruby!
    ⭐ 4.1k on Github

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  45. We Need
    Some Magic!

    View full-size slide

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

    View full-size slide

  47. module Extensions
    module Visibility
    def visible?(context)
    return context[:current_user]&.is_a?(User)
    super
    end
    end
    end
    BaseType.prepend(Extensions!::Visibility)
    Visibility Extension

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  50. class UserType < BaseType
    field :posts, PostType.connection_type
    with_options login_required: true do
    field :profile, ProfileType
    end
    end
    Visibility Extension
    #

    View full-size slide

  51. CommentType
    Why Is Visibility Better
    Then Other Ways?

    View full-size slide

  52. Why Is Visibility Better?
    CommentTyp
    # GraphQL query
    {
    !__schema {
    types {
    name
    }
    }
    }
    GraphQL Introspection

    View full-size slide

  53. AppSchema#execute
    Types--
    Mutations--
    POST /graphql
    TagType
    UserType
    PostType ImageType
    ProfileType
    CommentType
    CreatePostMutation
    AddCommentMutation
    UpdateProfileMutation
    GraphqlController
    Hide Parts of Your Schema

    View full-size slide

  54. Authorization

    View full-size slide

  55. Authorization
    CAN this
    HOMER HAVE A
    BEER?

    View full-size slide

  56. Authorization
    CAN this
    HOMER HAVE A
    BEER?

    View full-size slide

  57. class UserType < BaseType
    field :posts, PostType.connection_type
    with_options login_required: true, admin: true do
    field :profile, ProfileType
    end
    end
    And Again Visibility?

    View full-size slide

  58. Authorization is Flexible
    (current_user has role) !||
    (current_user has permission) !||
    (business rule) !||
    !!...

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  61. Authorization
    Auth Library
    API
    Abilities Layer
    Helpers (e.g. for templates)
    ...
    (e.g. cancan
    or pundit)
    GraphQL
    RESTfull
    ? How to use Library ?

    View full-size slide

  62. 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
    !!...

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  65. AppSchema#execute
    Types--
    Mutations--
    POST /graphql
    TagType
    UserType
    PostType ImageType
    ProfileType
    CommentType
    CreatePostMutation
    AddCommentMutation
    UpdateProfileMutation
    GraphqlController
    Ability For Each Node
    author (String)
    body (String)

    View full-size slide

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

    View full-size slide

  67. Can We (re)Use Existing
    Abilities Layer?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  70. Who Has Heard about
    gem "action_policy"?

    View full-size slide

  71. gem "action_policy"
    The Authorization Framework

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  75. # 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

    View full-size slide

  76. # All Docs
    GraphQL Features

    View full-size slide

  77. AppSchema#execute
    Types--
    Mutations--
    POST /graphql
    TagType
    UserType
    PostType ImageType
    ProfileType
    CommentType
    CreatePostMutation
    AddCommentMutation
    UpdateProfileMutation
    GraphqlController
    Scoping Data
    admin?
    manager?
    owner?
    guest?

    View full-size slide

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

    View full-size slide

  79. UI/UX
    CAN
    HOMER PRESS
    THE BUTTON?

    View full-size slide

  80. Where is the best place for UX element display logic?
    On Backend
    On Frontend
    On Both sides
    UI/UX
    1.
    2.
    3.

    View full-size slide

  81. On Backend
    On Frontend
    On Both sides
    UI/UX



    Expose
    Where is the best place for UX element display logic?

    View full-size slide

  82. class PostType < BaseType
    expose_authorization_rules :edit?, :destroy?
    end
    UI/UX

    View full-size slide

  83. UI/UX
    {
    post(id: $id) {
    canEdit {
    value # bool
    message # top-level decline message
    reasons { # detailed information
    details
    fullMessage
    }
    }
    !!...
    }

    View full-size slide

  84. Hot Abilities

    View full-size slide

  85. AppSchema#execute
    Types--
    Mutations--
    POST /graphql
    TagType
    UserType
    PostType ImageType
    ProfileType
    CommentType
    CreatePostMutation
    AddCommentMutation
    UpdateProfileMutation
    GraphqlController
    Hot Abilities
    can :edit?

    View full-size slide

  86. class UserType < BaseType
    field :profile, ProfileType, authorize: true
    end
    class ProfilePolicy < ApplicationPolicy
    cache :show?
    def show?
    # heavy calculation
    end
    end
    Let's Cache it!

    View full-size slide

  87. Why We Are Talking
    Only About Reading Data?

    View full-size slide

  88. AppSchema#execute
    GraphQL Internals
    Types--
    Mutations--
    POST /graphql
    TagType
    UserType
    PostType ImageType
    ProfileType
    CommentType
    CreatePostMutation
    AddCommentMutation
    UpdateProfileMutation
    GraphqlController

    View full-size slide

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

    View full-size slide

  90. 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?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  98. Conclusion
    Shared
    Auth*
    Libraries
    Features
    Features
    RESTful GraphQL

    View full-size slide

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

    View full-size slide

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



    View full-size slide

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

    View full-size slide