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 Slide

  2. View Slide

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

    View Slide

  4. View Slide

  5. View Slide

  6. evilmartians.com

    View Slide

  7. TATARSTAN

    View Slide

  8. Authorization
    90 %
    GraphQL
    10 %
    Buzzwords 

    of 

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

    View Slide

  9. https://book.productionreadygraphql.com/

    View Slide

  10. GraphQL Auth
    through the prism of
    RESTful Auth

    View Slide

  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

    View 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
    User

    View Slide

  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

    View Slide

  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

    View Slide

  15. Rails DHH way
    it's me

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  23. GraphQL Internals

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  30. When You Read
    First Pages of
    GraphQL
    Documentation

    View Slide

  31. *ACCESS
    CONTROL

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

    View Slide

  32. Authentication before
    authorization

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    LAYER
    GET /users
    Ensuring Authentication

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  49. We Need
    Some Magic!

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  55. CommentType
    Why Is Visibility Better
    Then Other Ways?

    View Slide

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

    View Slide

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

    View Slide

  58. Authorization

    View Slide

  59. Authorization
    CAN this
    HOMER HAVE A
    BEER?

    View Slide

  60. Authorization
    CAN this
    HOMER HAVE A
    BEER?

    View Slide

  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?

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  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)

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  74. GraphQL Pro

    View Slide

  75. Who Has Heard about
    gem "action_policy"?

    View Slide

  76. gem "action_policy"
    The Authorization Framework

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  81. # All Docs
    GraphQL Features

    View Slide

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

    View Slide

  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

    View Slide

  84. UI/UX
    CAN
    HOMER PRESS
    THE BUTTON?

    View Slide

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

    View Slide

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



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

    View Slide

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

    View Slide

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

    View Slide

  89. Hot Abilities

    View Slide

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

    View Slide

  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!

    View Slide

  92. Why We Are Talking
    Only About Reading Data?

    View Slide

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

    View Slide

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

    View Slide

  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?

    View Slide

  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

    View Slide

  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

    View Slide

  98. Testing

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  104. Conclusion

    View Slide

  105. Conclusion
    Shared
    Auth*
    Libraries
    Features
    Features
    RESTful GraphQL

    View Slide

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

    View Slide

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



    View Slide

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

    View Slide