Search code examples
ruby-on-railsgraphql-ruby

Graphql-ruby scoping in habtm, data leak in nested query


Here's an example of a data leak that occurs if using standard graphql-ruby setup.

Using the graphql nested request below, the response returns data nested under company 1, that belongs to company 2. I expect the response to be limited to the the accountants log that belongs to the company it is nested within.

That way it is, it is leaking information.

The question is how do we patch that leak so the only data returned in the response and its nested objects is data that belongs to the company (the root object).

This query:

query { 
  company(id: "1") {
    id
    name
    activityLog {
      id
      activityAt
      accountant {
        id
        name
      }
      companyId
    }
    accountants {
      id
      name
      activityLog {
        id
        activityAt
        companyId
      }
    }
  }
}

returns this response:

{
  "data": {
    "company": {
      "id": "1",
      "name": "AwsomeCo",
      "activityLog": [
        {
          "id": "1",
          "activityAt": "2019-10-12 16:40:13 UTC",
          "accountant": {
            "id": "100",
            "name": "Mr Smith",
          },
          "companyId": "1"
        }
      ],
      "accountants": [
        {
          "id": "100",
          "name": "Mr Smith"
          "activityLog": [
            {
              "id": "1",
              "activityAt": "2019-10-12 16:40:13 UTC",
              "companyId": "1"
            },
            {
              "id": "2",
              "activityAt": "2019-10-11 16:40:13 UTC",
              "companyId": "2"  // OTHER COMPANY, NEED TO PRESERVE PARENT SCOPE
            },
          ],
        }
      }
    }
  }
}

leaking transaction log data of company 2, within the nested elements of company 1.

Again, the question is: How do we preserve scope, only displaying data in context of the company it is displaying?

Code to reproduce:

GraphQL types (using graphql-ruby gem)

#query_type.rb
module Types
  class QueryType < Types::BaseObject
    # Add root-level fields here.
    # They will be entry points for queries on your schema.
    field :company_leak, Types::ExampleLeak::CompanyType, null: false do
      argument :id, ID, required: true
    end
    field :companies_leak, [ Types::ExampleLeak::CompanyType ], null: false

    def company_leak(id: )
      Company.find(id)
    end

    def companies_leak
      Company.all
    end
  end
end

module Types
  module ExampleLeak
    class CompanyType < BaseObject
      field :id, ID, null: false
      field :name, String, null: false
      field :transaction_logs, [Types::ExampleLeak::TransactionLogType], null: true
      field :accountants, [Types::ExampleLeak::AccountantType], null: true
    end
  end
end

module Types
  module ExampleLeak
    class AccountantType < BaseObject
      field :id, ID, null: false
      field :name, String, null: false
      field :transaction_logs, [Types::ExampleLeak::TransactionLogType], null: true
      field :companies, [Types::ExampleLeak::CompanyType], null: true
    end
  end
end

module Types
  module ExampleLeak
    class TransactionLogType < BaseObject
      field :id, ID, null: false
      field :activity_at, String, null: false
      field :company_id, ID, null: false
      field :accountant, Types::ExampleLeak::AccountantType, null: false
    end
  end
end

Models

class Company < ApplicationRecord
  has_and_belongs_to_many :accountants
  has_many :transaction_logs
end

class Accountant < ApplicationRecord
  has_and_belongs_to_many :companies
  has_many :transaction_logs
end

class TransactionLog < ApplicationRecord
  belongs_to :company
  belongs_to :accountant
end

seeds.rb

awesomeco = Company.create!(name: 'AwesomeCo')
boringco = Company.create!(name: 'BoringCo') 
mr_smith = Accountant.create!(name: "Mr. Smith")
awesomeco.accountants << mr_smith
boringco.accountants << mr_smith
mr_smith.transaction_logs.create!(company: awesomeco, activity_at: 1.day.ago)
mr_smith.transaction_logs.create!(company: boringco, activity_at: 2.days.ago)

Public repo containing full code, intended as educational resource:

https://github.com/rubynor/graphql-ruby-training-ground


Solution

  • We can update field in class AccountantType < BaseObject as follows to resolve transaction logs:

      field :transaction_logs, [Types::ExampleLeak::TransactionLogType], null: true,
        resolve: ->(obj, args, ctx) {
          company_leak = ctx.irep_node.parent.parent.arguments[:id]
          companies_leak = ctx.parent.parent.object.object.id
          TransactionLog.where(id: company_leak.present? ? company_leak : companies_leak)
        }
    

    If the company ID is given an argument, It will fetch transaction logs against that id, otherwise with respect to its parent Accountant