Module: Immunio::QueryingHooks

Extended by:
ActiveSupport::Concern
Defined in:
lib/immunio/plugins/active_record_relation.rb

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.methods_to_wrapObject

Returns the value of attribute methods_to_wrap.



89
90
91
# File 'lib/immunio/plugins/active_record_relation.rb', line 89

def methods_to_wrap
  @methods_to_wrap
end

Class Method Details

.wrap_method(method) ⇒ Object

Wrap ActiveRecord::Relation methods to add API calls to the context data for a query. Some additional methods are wrapped because they execute queries. In those cases, we must associate the connection to the relation so we can grab the context info when we see the query being executed.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/immunio/plugins/active_record_relation.rb', line 98

def self.wrap_method(method)
  method_with_immunio = "#{method}_with_immunio".to_sym
  method_without_immunio = "#{method}_without_immunio".to_sym

  define_method method_with_immunio do |*args, &block|
    Request.time "plugin", "Immunio::RelationTracking" do
      # Construct the context data. The name method returns the name of the
      # class of self, which is the name of the model. This allows us to
      # differentiate between scopes from the same lines of code but on
      # different models.
      #
      # In Ruby 2 and up, we can ask for specific frames and the bits we
      # need from them. This doesn't impact performance much. But in earlier
      # versions of Ruby we would need to gather the entire stack trace and
      # parse the strings for the frames we need. As that is too much of a
      # performance hit, we don't include caller information in the
      # additional context data.
      if defined? caller_locations
        stack = caller_locations(4..5)

        # If the method was called on a relation directly, the real caller
        # is four frames up. But if the method was called on the model
        # class, like `User.where`, then there is an extra frame for
        # delegating the method call to the result of the `all` method. We
        # look for the telltale sign of the caller being in the querying.rb
        # file of ActiveRecord and with the same name as the method.
        if stack[0].path.end_with?('querying.rb') && stack[0].label == method.to_s
          frame = stack[1]
        else
          frame = stack[0]
        end

        caller_method = frame.label
        caller_line = frame.lineno
        checksum = Immunio::Context::FILE_CHECKSUM_CACHE[frame.path]

        data = "Relation for #{name}, method called: #{method}, caller: #{caller_method}:#{caller_line}"
        data << ", checksum: #{checksum}" unless checksum.blank?
      else
        data = "Relation for #{name}, method called: #{method}"
      end

      # In Rails 3, #where and #having clone the relation and return it with
      # the conditions added, but use the original relation to actually
      # build the conditions. We have to add a hack to push the last cloned
      # relation onto the connection's relation stack so parameters are
      # associated with the cloned relation and not the original relation.
      relation = if method == :build_where && Rails::VERSION::MAJOR == 3
        QueryTracker.instance.last_spawned_relation connection
      else
        self
      end

      # Push the current relation onto the stack of relations for the
      # connection. The top relation on the stack at query execution time is
      # used for contextual data.
      QueryTracker.instance.push_relation relation

      begin
        # Call original method
        ret = Request.pause "plugin", "Immunio::RelationTracking" do
          send method_without_immunio, *args, &block
        end

        # These two methods create clones of the original without actually
        # calling #clone. Copy relation data over in this special case.
        if method == :except || method == :only
          QueryTracker.instance.spawn_relation self, ret
        end

        # If ret is a relation, such as when #where is called, we need to
        # add data about the API call to the relation. Otherwise, the API
        # call is returning actual data from a query execution, like #count.
        # We don't need to add context data about calls that execute queries
        # because the stack trace at query execution time will include this
        # call.
        if ret.is_a? ActiveRecord::Relation
          QueryTracker.instance.add_relation_data ret, data
        end
      ensure
        QueryTracker.instance.pop_relation relation
      end

      ret
    end
  end
end