request_context as a gem
refs FOO-1649 Pull out middleware for request context to a gem so that other engines in canvas can use the generator to look at the current request for standard attributes in the same way. TEST PLAN: 1) requests should keep on getting context ids 2) sessions should keep getting added to the cookie jar Change-Id: I9245491f722ac29c9544623ee14e0771ae248cd4 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/259609 Reviewed-by: Cody Cutrer <cody@instructure.com> Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> QA-Review: Ethan Vizitei <evizitei@instructure.com> Product-Review: Ethan Vizitei <evizitei@instructure.com>
This commit is contained in:
parent
9f455d95d1
commit
dd58f89f2c
|
@ -171,6 +171,7 @@ path 'gems' do
|
|||
gem 'lti_outbound'
|
||||
gem 'multipart'
|
||||
gem 'paginated_collection'
|
||||
gem 'request_context'
|
||||
gem 'stringify_ids'
|
||||
gem 'twitter'
|
||||
gem 'utf8_cleaner'
|
||||
|
|
|
@ -622,7 +622,7 @@ class ApplicationController < ActionController::Base
|
|||
Canvas::Apm.annotate_trace(
|
||||
Shard.current,
|
||||
@domain_root_account,
|
||||
RequestContextGenerator.request_id,
|
||||
RequestContext::Generator.request_id,
|
||||
@current_user
|
||||
)
|
||||
end
|
||||
|
@ -676,7 +676,7 @@ class ApplicationController < ActionController::Base
|
|||
headers['X-Frame-Options'] = 'SAMEORIGIN'
|
||||
end
|
||||
headers['Strict-Transport-Security'] = 'max-age=31536000' if request.ssl?
|
||||
RequestContextGenerator.store_request_meta(request, @context)
|
||||
RequestContext::Generator.store_request_meta(request, @context)
|
||||
true
|
||||
end
|
||||
|
||||
|
@ -1444,7 +1444,7 @@ class ApplicationController < ActionController::Base
|
|||
return unless (request.xhr? || request.put?) && params[:page_view_token] && !updated_fields.empty?
|
||||
return unless page_views_enabled?
|
||||
|
||||
RequestContextGenerator.store_interaction_seconds_update(
|
||||
RequestContext::Generator.store_interaction_seconds_update(
|
||||
params[:page_view_token],
|
||||
updated_fields[:interaction_seconds]
|
||||
)
|
||||
|
@ -1495,7 +1495,7 @@ class ApplicationController < ActionController::Base
|
|||
@page_view.account_id = @domain_root_account.id
|
||||
@page_view.developer_key_id = @access_token.try(:developer_key_id)
|
||||
@page_view.store
|
||||
RequestContextGenerator.store_page_view_meta(@page_view)
|
||||
RequestContext::Generator.store_page_view_meta(@page_view)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -155,7 +155,7 @@ class ErrorsController < ApplicationController
|
|||
end
|
||||
report.backtrace = backtrace
|
||||
report.http_env ||= Canvas::Errors::Info.useful_http_env_stuff_from_request(request)
|
||||
report.request_context_id = RequestContextGenerator.request_id
|
||||
report.request_context_id = RequestContext::Generator.request_id
|
||||
report.assign_data(error)
|
||||
report.save
|
||||
report.delay.send_to_external
|
||||
|
|
|
@ -16,104 +16,7 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require 'securerandom'
|
||||
require 'canvas_security'
|
||||
|
||||
class RequestContextGenerator
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
request_id = generate_request_id(env)
|
||||
|
||||
# rack.session.options (where the session_id is saved by our session
|
||||
# store) isn't availalbe at this point in the middleware stack. It is
|
||||
# lazily loaded the first time the session is accessed, so we won't get
|
||||
# session_ids in the log on the very first request (usually loading the
|
||||
# login page). It is written out to a cookie so that we can pick it up for
|
||||
# logs in subsequent requests. See RequestContextSession, we can't write it
|
||||
# to a cookie in this middleware because the cookie header has already been
|
||||
# written by the time this app.call returns.
|
||||
session_id = ActionDispatch::Request.new(env).cookie_jar[:log_session_id]
|
||||
meta_headers = +""
|
||||
Thread.current[:context] = {
|
||||
request_id: request_id,
|
||||
session_id: session_id,
|
||||
meta_headers: meta_headers,
|
||||
}
|
||||
|
||||
# logged here to get as close to the beginning of the request being
|
||||
# processed as possible
|
||||
RequestContextGenerator.store_request_queue_time(env['HTTP_X_REQUEST_START'])
|
||||
|
||||
status, headers, body = @app.call(env)
|
||||
|
||||
# The session id may have been reset in the request, in which case
|
||||
# we want to log the new one,
|
||||
session_id = (env['rack.session.options'] || {})[:id]
|
||||
headers['X-Session-Id'] = session_id if session_id
|
||||
headers['X-Request-Context-Id'] = request_id
|
||||
headers['X-Canvas-Meta'] = meta_headers if meta_headers.present?
|
||||
|
||||
[ status, headers, body ]
|
||||
end
|
||||
|
||||
def self.request_id
|
||||
Thread.current[:context].try(:[], :request_id)
|
||||
end
|
||||
|
||||
def self.add_meta_header(name, value)
|
||||
return if value.blank?
|
||||
meta_headers = Thread.current[:context].try(:[], :meta_headers)
|
||||
return if !meta_headers
|
||||
meta_headers << "#{name}=#{value};"
|
||||
end
|
||||
|
||||
def self.store_interaction_seconds_update(token, interaction_seconds)
|
||||
data = CanvasSecurity::PageViewJwt.decode(token)
|
||||
if data
|
||||
self.add_meta_header("r", "#{data[:request_id]}|#{data[:created_at]}|#{interaction_seconds}")
|
||||
end
|
||||
end
|
||||
|
||||
def self.store_request_queue_time(header_val)
|
||||
if header_val
|
||||
match = header_val.match(/t=(?<req_start>\d+)/)
|
||||
return unless match
|
||||
|
||||
delta = (Time.now.utc.to_f * 1000000).to_i - match['req_start'].to_i
|
||||
RequestContextGenerator.add_meta_header("q", delta)
|
||||
end
|
||||
end
|
||||
|
||||
def self.store_request_meta(request, context)
|
||||
self.add_meta_header("o", request.path_parameters[:controller])
|
||||
self.add_meta_header("n", request.path_parameters[:action])
|
||||
if context
|
||||
self.add_meta_header("t", context.class)
|
||||
self.add_meta_header("i", context.id)
|
||||
end
|
||||
end
|
||||
|
||||
def self.store_page_view_meta(page_view)
|
||||
self.add_meta_header("x", page_view.interaction_seconds)
|
||||
self.add_meta_header("p", page_view.participated? ? "t" : "f")
|
||||
self.add_meta_header("e", page_view.asset_user_access_id)
|
||||
self.add_meta_header("f", page_view.created_at.try(:utc).try(:iso8601, 2))
|
||||
end
|
||||
|
||||
private
|
||||
def generate_request_id(env)
|
||||
if env['HTTP_X_REQUEST_CONTEXT_ID'] && env['HTTP_X_REQUEST_CONTEXT_SIGNATURE']
|
||||
request_context_id = Canvas::Security.base64_decode(env['HTTP_X_REQUEST_CONTEXT_ID'])
|
||||
request_context_signature = Canvas::Security.base64_decode(env['HTTP_X_REQUEST_CONTEXT_SIGNATURE'])
|
||||
if Canvas::Security.verify_hmac_sha512(request_context_id, request_context_signature)
|
||||
return request_context_id
|
||||
else
|
||||
Rails.logger.info("ignoring X-Request-Context-Id header, signature could not be verified")
|
||||
end
|
||||
end
|
||||
SecureRandom.uuid
|
||||
end
|
||||
end
|
||||
# TODO: This class is being relocated to the request_context gem
|
||||
# in the gems directory, this shim will remain until all callsites
|
||||
# are transitioned.
|
||||
RequestContextGenerator = RequestContext::Generator
|
||||
|
|
|
@ -16,23 +16,7 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
class RequestContextSession
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
status, headers, body = @app.call(env)
|
||||
|
||||
session_id = (env['rack.session.options'] || {})[:id]
|
||||
if session_id
|
||||
ActionDispatch::Request.new(env).cookie_jar[:log_session_id] = {
|
||||
:value => session_id,
|
||||
:secure => CanvasRails::Application.config.session_options[:secure],
|
||||
:httponly => true
|
||||
}
|
||||
end
|
||||
|
||||
[ status, headers, body ]
|
||||
end
|
||||
end
|
||||
# TODO: This class is being relocated to the request_context gem
|
||||
# in the gems directory, this shim will remain until all callsites
|
||||
# are transitioned.
|
||||
RequestContextSession = RequestContext::Session
|
||||
|
|
|
@ -204,11 +204,11 @@ class RequestThrottle
|
|||
end
|
||||
|
||||
def report_on_stats(db_runtime, account, starting_mem, ending_mem, user_cpu, system_cpu)
|
||||
RequestContextGenerator.add_meta_header("b", starting_mem)
|
||||
RequestContextGenerator.add_meta_header("m", ending_mem)
|
||||
RequestContextGenerator.add_meta_header("u", "%.2f" % [user_cpu])
|
||||
RequestContextGenerator.add_meta_header("y", "%.2f" % [system_cpu])
|
||||
RequestContextGenerator.add_meta_header("d", "%.2f" % [db_runtime])
|
||||
RequestContext::Generator.add_meta_header("b", starting_mem)
|
||||
RequestContext::Generator.add_meta_header("m", ending_mem)
|
||||
RequestContext::Generator.add_meta_header("u", "%.2f" % [user_cpu])
|
||||
RequestContext::Generator.add_meta_header("y", "%.2f" % [system_cpu])
|
||||
RequestContext::Generator.add_meta_header("d", "%.2f" % [db_runtime])
|
||||
|
||||
if account&.shard&.database_server
|
||||
InstStatsd::Statsd.timing("requests_system_cpu.cluster_#{account.shard.database_server.id}", system_cpu,
|
||||
|
|
|
@ -24,6 +24,6 @@ class Auditors::Record < EventStream::Record
|
|||
def initialize(*args)
|
||||
super(*args)
|
||||
|
||||
self.request_id ||= RequestContextGenerator.request_id unless event_type == 'corrupted'
|
||||
self.request_id ||= RequestContext::Generator.request_id unless event_type == 'corrupted'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -54,7 +54,7 @@ class PageView < ActiveRecord::Base
|
|||
p.interaction_seconds = 5
|
||||
p.created_at = Time.now
|
||||
p.updated_at = Time.now
|
||||
p.id = RequestContextGenerator.request_id
|
||||
p.id = RequestContext::Generator.request_id
|
||||
p.export_columns.each do |c|
|
||||
v = p.send(c)
|
||||
if !v.nil? && v.respond_to?(:force_encoding)
|
||||
|
|
|
@ -342,8 +342,8 @@ module CanvasRails
|
|||
# we've loaded config/initializers/session_store.rb
|
||||
initializer("extend_middleware_stack", after: :load_config_initializers) do |app|
|
||||
app.config.middleware.insert_before(config.session_store, LoadAccount)
|
||||
app.config.middleware.swap(ActionDispatch::RequestId, RequestContextGenerator)
|
||||
app.config.middleware.insert_after(config.session_store, RequestContextSession)
|
||||
app.config.middleware.swap(ActionDispatch::RequestId, RequestContext::Generator)
|
||||
app.config.middleware.insert_after(config.session_store, RequestContext::Session)
|
||||
app.config.middleware.insert_before(Rack::Head, RequestThrottle)
|
||||
app.config.middleware.insert_before(Rack::MethodOverride, PreventNonMultipartParse)
|
||||
end
|
||||
|
|
|
@ -29,7 +29,7 @@ if config[:components].present?
|
|||
attr_accessor :migration, :rake_task
|
||||
|
||||
def context_id
|
||||
RequestContextGenerator.request_id
|
||||
RequestContext::Generator.request_id
|
||||
end
|
||||
|
||||
def job_tag
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gemspec
|
||||
|
||||
gem 'canvas_security', path: '../canvas_security'
|
||||
gem 'canvas_cache', path: '../canvas_cache'
|
||||
gem 'dynamic_settings', path: '../dynamic_settings'
|
||||
gem 'config_file', path: '../config_file'
|
|
@ -0,0 +1,46 @@
|
|||
# RequestContext
|
||||
|
||||
Keeping the state for canvas requests.
|
||||
|
||||
## Usage
|
||||
|
||||
RequestContext is a combination of [middleware](https://guides.rubyonrails.org/rails_on_rack.html)
|
||||
for rails and a kind of global "what is my current state" access
|
||||
from anywhere.
|
||||
|
||||
#### Generator
|
||||
|
||||
RequestContext::Generator, when added to the middleware stack
|
||||
for an application, makes sure that every request has a request_context_id
|
||||
on it:
|
||||
|
||||
```ruby
|
||||
# application.rb
|
||||
initializer("extend_middleware_stack") do |app|
|
||||
app.config.middleware.swap(ActionDispatch::RequestId, RequestContext::Generator)\
|
||||
app.config.middleware.insert_after(config.session_store, RequestContext::Session)
|
||||
end
|
||||
```
|
||||
|
||||
Why replace the build in RequestId middleware? this one reads the incoming environment
|
||||
for potentially provided request IDs from other services in the
|
||||
'HTTP_X_REQUEST_CONTEXT_ID' header. This class also provides an interface
|
||||
for accessing the current request id from pretty much anywhere in the app
|
||||
without having to know where it's stored (we use the thread context):
|
||||
|
||||
```ruby
|
||||
RequestContext::Generator.request_id
|
||||
```
|
||||
|
||||
In order for this to work, you need the cookie jar to have
|
||||
the right data loaded into it, which you can find being written
|
||||
in the RequestContext::Session middleware. The
|
||||
reason is middleware ordering, the Generator runs after
|
||||
the cookie jar has already been written to on the way out.
|
||||
|
||||
You need to use both.
|
||||
|
||||
## Running Tests
|
||||
|
||||
This gem is tested with rspec. You can use `test.sh` to run it, or
|
||||
do it yourself with `bundle exec rspec spec`.
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# Copyright (C) 2021 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require 'request_context/generator'
|
||||
require 'request_context/session'
|
||||
|
||||
module RequestContext
|
||||
end
|
|
@ -0,0 +1,154 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# Copyright (C) 2011 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require 'securerandom'
|
||||
require 'canvas_security'
|
||||
|
||||
# this adds the cookie_jar method to the
|
||||
# action dispatch request, if it's not loaded
|
||||
# then there is no "cookie_jar"
|
||||
require 'action_dispatch/middleware/cookies'
|
||||
|
||||
module RequestContext
|
||||
##
|
||||
# RequestContext::Generator is a rails middleware for making
|
||||
# sure every request into the app has a unique request id,
|
||||
# either provided in the "env" hash already from an HTTP header
|
||||
# (which must be properly signed to be valid), in the case of a request
|
||||
# initiating from another service inside the canvas ecosystem, or
|
||||
# generating a new one. Either way we store that current request
|
||||
# on the current thread and other areas of the app can use this class to
|
||||
# read the current request ID without having to know the way it's stored.
|
||||
#
|
||||
# This also is a convenient place to add additional header info that
|
||||
# we want to leave with the response without having to pipe it through
|
||||
# every layer in between (see ".add_meta_header")
|
||||
class Generator
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
request_id = generate_request_id(env)
|
||||
|
||||
# rack.session.options (where the session_id is saved by our session
|
||||
# store) isn't availalbe at this point in the middleware stack. It is
|
||||
# lazily loaded the first time the session is accessed, so we won't get
|
||||
# session_ids in the log on the very first request (usually loading the
|
||||
# login page). It is written out to a cookie so that we can pick it up for
|
||||
# logs in subsequent requests. See RequestContextSession, we can't write it
|
||||
# to a cookie in this middleware because the cookie header has already been
|
||||
# written by the time this app.call returns.
|
||||
session_id = ActionDispatch::Request.new(env).cookie_jar[:log_session_id]
|
||||
meta_headers = +""
|
||||
Thread.current[:context] = {
|
||||
request_id: request_id,
|
||||
session_id: session_id,
|
||||
meta_headers: meta_headers,
|
||||
}
|
||||
|
||||
# logged here to get as close to the beginning of the request being
|
||||
# processed as possible
|
||||
RequestContext::Generator.store_request_queue_time(env['HTTP_X_REQUEST_START'])
|
||||
|
||||
status, headers, body = @app.call(env)
|
||||
|
||||
# The session id may have been reset in the request, in which case
|
||||
# we want to log the new one,
|
||||
session_id = (env['rack.session.options'] || {})[:id]
|
||||
headers['X-Session-Id'] = session_id if session_id
|
||||
headers['X-Request-Context-Id'] = request_id
|
||||
headers['X-Canvas-Meta'] = meta_headers if meta_headers.present?
|
||||
|
||||
[ status, headers, body ]
|
||||
end
|
||||
|
||||
def self.request_id
|
||||
Thread.current[:context].try(:[], :request_id)
|
||||
end
|
||||
|
||||
def self.add_meta_header(name, value)
|
||||
return if value.blank?
|
||||
meta_headers = Thread.current[:context].try(:[], :meta_headers)
|
||||
return if !meta_headers
|
||||
meta_headers << "#{name}=#{value};"
|
||||
end
|
||||
|
||||
def self.store_interaction_seconds_update(token, interaction_seconds)
|
||||
data = CanvasSecurity::PageViewJwt.decode(token)
|
||||
if data
|
||||
self.add_meta_header("r", "#{data[:request_id]}|#{data[:created_at]}|#{interaction_seconds}")
|
||||
end
|
||||
end
|
||||
|
||||
def self.store_request_queue_time(header_val)
|
||||
if header_val
|
||||
match = header_val.match(/t=(?<req_start>\d+)/)
|
||||
return unless match
|
||||
|
||||
delta = (Time.now.utc.to_f * 1000000).to_i - match['req_start'].to_i
|
||||
RequestContext::Generator.add_meta_header("q", delta)
|
||||
end
|
||||
end
|
||||
|
||||
def self.store_request_meta(request, context)
|
||||
self.add_meta_header("o", request.path_parameters[:controller])
|
||||
self.add_meta_header("n", request.path_parameters[:action])
|
||||
if context
|
||||
self.add_meta_header("t", context.class)
|
||||
self.add_meta_header("i", context.id)
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# store_page_view_meta takes a specific set of attributes
|
||||
# we care about from an interaction and maps them to pre-defined
|
||||
# single-character meta headers. This can be read and consumed
|
||||
# by the logging pipeline downstream.
|
||||
#
|
||||
# PageView duck type:
|
||||
# @field [Float] interaction_seconds
|
||||
# @field [Boolean] participated?
|
||||
# @field [Int] asset_user_access_id
|
||||
# @field [DateTime] created_at
|
||||
#
|
||||
# @param [PageView] the bundle of attributes we want to map to meta headers
|
||||
def self.store_page_view_meta(page_view)
|
||||
self.add_meta_header("x", page_view.interaction_seconds)
|
||||
self.add_meta_header("p", page_view.participated? ? "t" : "f")
|
||||
self.add_meta_header("e", page_view.asset_user_access_id)
|
||||
self.add_meta_header("f", page_view.created_at.try(:utc).try(:iso8601, 2))
|
||||
end
|
||||
|
||||
private
|
||||
def generate_request_id(env)
|
||||
if env['HTTP_X_REQUEST_CONTEXT_ID'] && env['HTTP_X_REQUEST_CONTEXT_SIGNATURE']
|
||||
request_context_id = CanvasSecurity.base64_decode(env['HTTP_X_REQUEST_CONTEXT_ID'])
|
||||
request_context_signature = CanvasSecurity.base64_decode(env['HTTP_X_REQUEST_CONTEXT_SIGNATURE'])
|
||||
if CanvasSecurity.verify_hmac_sha512(request_context_id, request_context_signature)
|
||||
return request_context_id
|
||||
else
|
||||
Rails.logger.info("ignoring X-Request-Context-Id header, signature could not be verified")
|
||||
end
|
||||
end
|
||||
SecureRandom.uuid
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# Copyright (C) 2014 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
module RequestContext
|
||||
class Session
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
status, headers, body = @app.call(env)
|
||||
|
||||
session_id = (env['rack.session.options'] || {})[:id]
|
||||
if session_id
|
||||
ActionDispatch::Request.new(env).cookie_jar[:log_session_id] = {
|
||||
:value => session_id,
|
||||
:secure => Rails.application.config.session_options[:secure],
|
||||
:httponly => true
|
||||
}
|
||||
end
|
||||
|
||||
[ status, headers, body ]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
# coding: utf-8
|
||||
lib = File.expand_path('../lib', __FILE__)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "request_context"
|
||||
spec.version = "0.1.0"
|
||||
spec.authors = ["Ethan Vizitei"]
|
||||
spec.email = ["evizitei@instructure.com"]
|
||||
spec.summary = %q{Instructure gem for managing http request metadata}
|
||||
|
||||
spec.files = Dir.glob("{lib,spec}/**/*") + %w(test.sh)
|
||||
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
||||
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
||||
spec.require_paths = ["lib"]
|
||||
|
||||
spec.add_dependency 'canvas_security'
|
||||
spec.add_dependency 'actionpack'
|
||||
spec.add_dependency 'railties'
|
||||
|
||||
spec.add_development_dependency 'bundler'
|
||||
spec.add_development_dependency 'byebug'
|
||||
spec.add_development_dependency 'rspec'
|
||||
spec.add_development_dependency 'timecop'
|
||||
|
||||
end
|
|
@ -17,46 +17,69 @@
|
|||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
|
||||
require 'spec_helper'
|
||||
require 'dynamic_settings'
|
||||
require 'timecop'
|
||||
|
||||
describe "RequestContextGenerator" do
|
||||
describe "RequestContext::Generator" do
|
||||
let(:env) { {} }
|
||||
let(:request) { double('Rack::Request', path_parameters: { controller: 'users', action: 'index' }) }
|
||||
let(:context) { double('Course', class: 'Course', id: 15) }
|
||||
|
||||
it "should generate the X-Canvas-Meta response header" do
|
||||
_, headers, _ = RequestContextGenerator.new(->(env) {
|
||||
RequestContextGenerator.add_meta_header("a1", "test1")
|
||||
RequestContextGenerator.add_meta_header("a2", "test2")
|
||||
RequestContextGenerator.add_meta_header("a3", "")
|
||||
_, headers, _ = RequestContext::Generator.new(->(env) {
|
||||
RequestContext::Generator.add_meta_header("a1", "test1")
|
||||
RequestContext::Generator.add_meta_header("a2", "test2")
|
||||
RequestContext::Generator.add_meta_header("a3", "")
|
||||
[ 200, {}, [] ]
|
||||
}).call(env)
|
||||
expect(headers['X-Canvas-Meta']).to eq "a1=test1;a2=test2;"
|
||||
end
|
||||
|
||||
it "should add request data to X-Canvas-Meta" do
|
||||
_, headers, _ = RequestContextGenerator.new(->(env) {
|
||||
RequestContextGenerator.add_meta_header("a1", "test1")
|
||||
RequestContextGenerator.store_request_meta(request, nil)
|
||||
_, headers, _ = RequestContext::Generator.new(->(env) {
|
||||
RequestContext::Generator.add_meta_header("a1", "test1")
|
||||
RequestContext::Generator.store_request_meta(request, nil)
|
||||
[ 200, {}, [] ]
|
||||
}).call(env)
|
||||
expect(headers['X-Canvas-Meta']).to eq "a1=test1;o=users;n=index;"
|
||||
end
|
||||
|
||||
it "should add request and context data to X-Canvas-Meta" do
|
||||
_, headers, _ = RequestContextGenerator.new(->(env) {
|
||||
RequestContextGenerator.add_meta_header("a1", "test1")
|
||||
RequestContextGenerator.store_request_meta(request, context)
|
||||
_, headers, _ = RequestContext::Generator.new(->(env) {
|
||||
RequestContext::Generator.add_meta_header("a1", "test1")
|
||||
RequestContext::Generator.store_request_meta(request, context)
|
||||
[ 200, {}, [] ]
|
||||
}).call(env)
|
||||
expect(headers['X-Canvas-Meta']).to eq "a1=test1;o=users;n=index;t=Course;i=15;"
|
||||
end
|
||||
|
||||
it "should add page view data to X-Canvas-Meta" do
|
||||
pv = page_view_model
|
||||
_, headers, _ = RequestContextGenerator.new(->(_env) {
|
||||
RequestContextGenerator.add_meta_header("a1", "test1")
|
||||
RequestContextGenerator.store_page_view_meta(pv)
|
||||
fake_pv_class = Class.new do
|
||||
def initialize(attrs)
|
||||
@attrs = attrs
|
||||
end
|
||||
|
||||
def interaction_seconds
|
||||
@attrs[:seconds]
|
||||
end
|
||||
|
||||
def participated?
|
||||
@attrs[:participated]
|
||||
end
|
||||
|
||||
def asset_user_access_id
|
||||
@attrs[:aua_id]
|
||||
end
|
||||
|
||||
def created_at
|
||||
@attrs[:created_at]
|
||||
end
|
||||
end
|
||||
pv = fake_pv_class.new({ seconds: 5.0, created_at: DateTime.now, participated: false})
|
||||
_, headers, _ = RequestContext::Generator.new(->(_env) {
|
||||
RequestContext::Generator.add_meta_header("a1", "test1")
|
||||
RequestContext::Generator.store_page_view_meta(pv)
|
||||
[ 200, {}, [] ]
|
||||
}).call(env)
|
||||
f = pv.created_at.try(:utc).try(:iso8601, 2)
|
||||
|
@ -65,13 +88,13 @@ describe "RequestContextGenerator" do
|
|||
|
||||
it "should generate a request_id and store it in Thread.current" do
|
||||
Thread.current[:context] = nil
|
||||
_, _, _ = RequestContextGenerator.new(->(env) {[200, {}, []]}).call(env)
|
||||
_, _, _ = RequestContext::Generator.new(->(env) {[200, {}, []]}).call(env)
|
||||
expect(Thread.current[:context][:request_id]).to be_present
|
||||
end
|
||||
|
||||
it "should add the request_id to X-Request-Context-Id" do
|
||||
Thread.current[:context] = nil
|
||||
_, headers, _ = RequestContextGenerator.new(->(env) {
|
||||
_, headers, _ = RequestContext::Generator.new(->(env) {
|
||||
[200, {}, []]
|
||||
}).call(env)
|
||||
expect(headers['X-Request-Context-Id']).to be_present
|
||||
|
@ -80,14 +103,14 @@ describe "RequestContextGenerator" do
|
|||
it "should find the session_id in a cookie and store it in Thread.current" do
|
||||
Thread.current[:context] = nil
|
||||
env['action_dispatch.cookies'] = { log_session_id: 'abc' }
|
||||
_, _, _ = RequestContextGenerator.new(->(env) {[200, {}, []]}).call(env)
|
||||
_, _, _ = RequestContext::Generator.new(->(env) {[200, {}, []]}).call(env)
|
||||
expect(Thread.current[:context][:session_id]).to eq 'abc'
|
||||
end
|
||||
|
||||
it "should find the session_id from the rack session and add it to X-Session-Id" do
|
||||
Thread.current[:context] = nil
|
||||
env['rack.session.options'] = { id: 'abc' }
|
||||
_, headers, _ = RequestContextGenerator.new(->(env) {
|
||||
_, headers, _ = RequestContext::Generator.new(->(env) {
|
||||
[200, {}, []]
|
||||
}).call(env)
|
||||
expect(headers['X-Session-Id']).to eq 'abc'
|
||||
|
@ -97,7 +120,7 @@ describe "RequestContextGenerator" do
|
|||
Timecop.freeze do
|
||||
Thread.current[:context] = nil
|
||||
env['HTTP_X_REQUEST_START'] = "t=#{(1.minute.ago.to_f * 1000000).to_i}"
|
||||
_, headers, _ = RequestContextGenerator.new(->(env) {
|
||||
_, headers, _ = RequestContext::Generator.new(->(env) {
|
||||
[200, {}, []]
|
||||
}).call(env)
|
||||
q = headers["X-Canvas-Meta"].match(/q=(\d+)/)[1].to_f
|
||||
|
@ -110,13 +133,16 @@ describe "RequestContextGenerator" do
|
|||
let(:remote_request_context_id){ '1234-5678-9012-3456-7890-1234-5678' }
|
||||
|
||||
let(:remote_signature) do
|
||||
Canvas::Security.sign_hmac_sha512(remote_request_context_id, shared_secret)
|
||||
CanvasSecurity.sign_hmac_sha512(remote_request_context_id, shared_secret)
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
# TODO: Probably shouldn't be stubbing this deep in a gem?
|
||||
# Maybe we should be passing in a config'd secret to CanvasSecurity
|
||||
# directly rather than monkeying with dynamic_settings.
|
||||
Thread.current[:context] = nil
|
||||
Canvas::DynamicSettings.reset_cache!
|
||||
Canvas::DynamicSettings.fallback_data = {
|
||||
DynamicSettings.reset_cache!
|
||||
DynamicSettings.fallback_data = {
|
||||
config: {
|
||||
canvas: {
|
||||
canvas: {
|
||||
|
@ -125,14 +151,14 @@ describe "RequestContextGenerator" do
|
|||
}
|
||||
}
|
||||
}
|
||||
env['HTTP_X_REQUEST_CONTEXT_ID'] = Canvas::Security.base64_encode(remote_request_context_id)
|
||||
env['HTTP_X_REQUEST_CONTEXT_SIGNATURE'] = Canvas::Security.base64_encode(remote_signature)
|
||||
env['HTTP_X_REQUEST_CONTEXT_ID'] = CanvasSecurity.base64_encode(remote_request_context_id)
|
||||
env['HTTP_X_REQUEST_CONTEXT_SIGNATURE'] = CanvasSecurity.base64_encode(remote_signature)
|
||||
end
|
||||
|
||||
after(:each){ Canvas::DynamicSettings.fallback_data = {} }
|
||||
after(:each){ DynamicSettings.fallback_data = {} }
|
||||
|
||||
def run_middleware
|
||||
_, headers, _msg = RequestContextGenerator.new(->(_){ [200, {}, []] }).call(env)
|
||||
_, headers, _msg = RequestContext::Generator.new(->(_){ [200, {}, []] }).call(env)
|
||||
headers
|
||||
end
|
||||
|
|
@ -17,12 +17,22 @@
|
|||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
|
||||
require 'spec_helper'
|
||||
|
||||
describe "RequestContextSession" do
|
||||
describe "RequestContext::Session" do
|
||||
it "should find the session_id from the rack session and add it a cookie" do
|
||||
app_class = Class.new do
|
||||
def config
|
||||
self
|
||||
end
|
||||
|
||||
def session_options
|
||||
{}
|
||||
end
|
||||
end
|
||||
Rails.application = app_class.new
|
||||
env = { 'rack.session.options' => { id: 'abc' } }
|
||||
_, headers, _ = RequestContextSession.new(->(env) {
|
||||
_, headers, _ = RequestContext::Session.new(->(env) {
|
||||
[200, {}, []]
|
||||
}).call(env)
|
||||
expect(env['action_dispatch.cookies']['log_session_id']).to eq 'abc'
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# Copyright (C) 2021 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
require 'byebug'
|
||||
require 'request_context'
|
||||
require 'rails'
|
||||
Rails.env = 'test'
|
||||
Rails.logger = Logger.new(STDOUT)
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.mock_with :rspec do |mocks|
|
||||
mocks.verify_partial_doubles = true
|
||||
end
|
||||
|
||||
config.order = 'random'
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
bundle check || bundle install
|
||||
bundle exec rspec spec
|
|
@ -137,8 +137,8 @@ module AuthenticationMethods
|
|||
validate_scopes
|
||||
@access_token.used!
|
||||
|
||||
RequestContextGenerator.add_meta_header('at', @access_token.global_id)
|
||||
RequestContextGenerator.add_meta_header('dk', @access_token.global_developer_key_id) if @access_token.developer_key_id
|
||||
RequestContext::Generator.add_meta_header('at', @access_token.global_id)
|
||||
RequestContext::Generator.add_meta_header('dk', @access_token.global_developer_key_id) if @access_token.developer_key_id
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ module Canvas
|
|||
@req = request
|
||||
@account = root_account
|
||||
@user = user
|
||||
@rci = opts.fetch(:request_context_id, RequestContextGenerator.request_id)
|
||||
@rci = opts.fetch(:request_context_id, RequestContext::Generator.request_id)
|
||||
@type = opts.fetch(:type, nil)
|
||||
@canvas_error_info = opts.fetch(:canvas_error_info, {})
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue