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:
Ethan Vizitei 2021-02-27 09:13:52 -06:00
parent 9f455d95d1
commit dd58f89f2c
22 changed files with 431 additions and 170 deletions

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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'

View 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`.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

4
gems/request_context/test.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
set -e
bundle check || bundle install
bundle exec rspec spec

View File

@ -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

View File

@ -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