canvas-lms/config/application.rb

374 lines
15 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
#
# Copyright (C) 2013 - 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_relative 'boot'
require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
# require "sprockets/railtie" # Do not enable the Rails Asset Pipeline
require "rails/test_unit/railtie"
Bundler.require(*Rails.groups)
# Zeitwerk does not HAVE to be enabled with rails 6
# Use this environment variable (which may have other file-based ways
# to trigger it later) in order to determine which autoloader we should
# use. The goal is to move to zeitwerk over time.
# https://guides.rubyonrails.org/autoloading_and_reloading_constants.html
# One way to set this in development would be to use your docker-compose.override.yml
# file to pass an env var to the web container:
#
# web:
# <<: *BASE
# environment:
# <<: *BASE-ENV
# VIRTUAL_HOST: .canvas.docker
# ...
# CANVAS_ZEITWERK: 1
unless defined?(CANVAS_ZEITWERK)
# choose to force zeitwerk on
# unless overridden with
# an env var or file
CANVAS_ZEITWERK = if ENV['CANVAS_ZEITWERK']
(ENV['CANVAS_ZEITWERK'] == '1')
elsif Rails.root && (zw_settings = ConfigFile.load("zeitwerk"))
zw_settings["enabled"]
else
true
end
end
module CanvasRails
class Application < Rails::Application
# this CANVAS_ZEITWERK constant flag is defined above in this file.
# It should be temporary,
# and removed once we've fully upgraded to zeitwerk autoloading,
# at which point the stuff inside this conditional block should remain.
if CANVAS_ZEITWERK
config.autoloader = :zeitwerk
# TODO: someday we can use this line, which will NOT
# add anything on the autoload paths the actual ruby
# $LOAD_PATH because zeitwerk will take care of anything
# we autolaod. This will make ACTUAL require statements
# that are necessary work faster because they'll have a smaller
# load path to scan.
# config.add_autoload_paths_to_load_path = false
end
$LOAD_PATH << config.root.to_s
config.encoding = 'utf-8'
require 'logging_filter'
config.filter_parameters.concat LoggingFilter.filtered_parameters
config.action_dispatch.rescue_responses['AuthenticationMethods::AccessTokenError'] = 401
config.action_dispatch.rescue_responses['AuthenticationMethods::AccessTokenScopeError'] = 401
config.action_dispatch.rescue_responses['AuthenticationMethods::LoggedOutError'] = 401
config.action_dispatch.rescue_responses['CanvasHttp::CircuitBreakerError'] = 502
config.action_dispatch.default_headers.delete('X-Frame-Options')
config.action_dispatch.default_headers['Referrer-Policy'] = 'no-referrer-when-downgrade'
config.action_controller.forgery_protection_origin_check = true
ActiveSupport.to_time_preserves_timezone = true
config.app_generators do |c|
c.test_framework :rspec
c.integration_tool :rspec
c.performance_tool :rspec
end
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
# See Rails::Configuration for more options.
# Make Time.zone default to the specified zone, and make Active Record store time values
# in the database in UTC, and return them converted to the specified local zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Comment line to use default local time.
config.time_zone = 'UTC'
log_config = File.exist?(Rails.root + 'config/logging.yml') && Rails.application.config_for(:logging).with_indifferent_access
log_config = { 'logger' => 'rails', 'log_level' => 'debug' }.merge(log_config || {})
opts = {}
require 'canvas_logger'
config.log_level = log_config['log_level']
log_level = ActiveSupport::Logger.const_get(config.log_level.to_s.upcase)
opts[:skip_thread_context] = true if log_config['log_context'] == false
case log_config["logger"]
when "syslog"
require 'syslog_wrapper'
log_config["app_ident"] ||= "canvas-lms"
log_config["daemon_ident"] ||= "canvas-lms-daemon"
facilities = 0
(log_config["facilities"] || []).each do |facility|
facilities |= Syslog.const_get "LOG_#{facility.to_s.upcase}"
end
ident = ENV['RUNNING_AS_DAEMON'] == 'true' ? log_config["daemon_ident"] : log_config["app_ident"]
opts[:include_pid] = true if log_config["include_pid"] == true
config.logger = SyslogWrapper.new(ident, facilities, opts)
config.logger.level = log_level
else
log_path = config.paths['log'].first
if ENV['RUNNING_AS_DAEMON'] == 'true'
log_path = Rails.root + 'log/delayed_job.log'
end
config.logger = CanvasLogger.new(log_path, log_level, opts)
end
# Activate observers that should always be running
config.active_record.observers = [:cacher, :stream_item_cache, :live_events_observer]
config.active_record.allow_unsafe_raw_sql = :disabled
config.active_support.encode_big_decimal_as_string = false
config.paths['lib'].eager_load!
config.paths.add('app/middleware', eager_load: true, autoload_once: true)
# prevent directory->module inference in these directories from wreaking
# havoc on the app (e.g. stylesheets/base -> ::Base)
config.eager_load_paths -= %W(#{Rails.root}/app/coffeescripts
#{Rails.root}/app/stylesheets
#{Rails.root}/ui)
config.middleware.use Rack::Chunked
config.middleware.use Rack::Deflater, if: ->(*) {
::Canvas::DynamicSettings.find(tree: :private)["enable_rack_deflation", failsafe: true]
}
config.middleware.use Rack::Brotli, if: ->(*) {
::Canvas::DynamicSettings.find(tree: :private)["enable_rack_brotli", failsafe: true]
}
config.i18n.load_path << Rails.root.join('config', 'locales', 'locales.yml')
config.i18n.load_path << Rails.root.join('config', 'locales', 'community.csv')
config.to_prepare do
require_dependency 'canvas/plugins/default_plugins'
Canvas::Plugins::DefaultPlugins.apply_all
ActiveSupport::JSON::Encoding.escape_html_entities_in_json = true
end
module PostgreSQLEarlyExtensions
module ConnectionHandling
def postgresql_connection(config)
conn_params = config.symbolize_keys
hosts = Array(conn_params[:host]).presence || [nil]
hosts.each_with_index do |host, index|
begin
conn_params[:host] = host
return super(conn_params)
# we _shouldn't_ be catching a NoDatabaseError, but that's what Rails raises
# for an error where the database name is in the message (i.e. a hostname lookup failure)
# CANVAS_RAILS6_0 rails 6.1 switches from PG::Error to ActiveRecord::ConnectionNotEstablished
# for any other error
rescue ::PG::Error, ::ActiveRecord::NoDatabaseError, ::ActiveRecord::ConnectionNotEstablished
raise if index == hosts.length - 1
# else try next host
end
end
end
end
def initialize(connection, logger, connection_parameters, config)
unless config.key?(:prepared_statements)
config = config.dup
config[:prepared_statements] = false
end
super(connection, logger, connection_parameters, config)
end
def connect
hosts = Array(@connection_parameters[:host]).presence || [nil]
hosts.each_with_index do |host, index|
begin
connection_parameters = @connection_parameters.dup
connection_parameters[:host] = host
@connection = PG::Connection.connect(connection_parameters)
configure_connection
raise "Canvas requires PostgreSQL 12 or newer" unless postgresql_version >= 12_00_00
break
rescue ::PG::Error => error
if error.message.include?("does not exist")
raise ActiveRecord::NoDatabaseError.new(error.message)
elsif index == hosts.length - 1
raise
end
# else try next host
end
end
end
end
module TypeMapInitializerExtensions
def query_conditions_for_initial_load
known_type_names = @store.keys.map { |n| "'#{n}'" } + @store.keys.map { |n| "'_#{n}'" }
<<~SQL % [known_type_names.join(", "),]
WHERE
t.typname IN (%s)
SQL
end
end
Autoextend.hook(:"ActiveRecord::Base",
PostgreSQLEarlyExtensions::ConnectionHandling,
singleton: true)
Autoextend.hook(:"ActiveRecord::ConnectionAdapters::PostgreSQLAdapter",
PostgreSQLEarlyExtensions,
method: :prepend)
Autoextend.hook(:"ActiveRecord::ConnectionAdapters::PostgreSQL::OID::TypeMapInitializer",
TypeMapInitializerExtensions,
method: :prepend)
module PatchThorWarning
# active_model_serializers should be passing `type: :boolean` here:
# https://github.com/rails-api/active_model_serializers/blob/v0.9.0.alpha1/lib/active_model/serializer/generators/serializer/scaffold_controller_generator.rb#L10
# but we don't really care about the warning, it only affects using the rails
# generator for a resource
#
# Easiest way to avoid the warning for now is to patch thor
def validate_default_type!
return if switch_name == "--serializer"
super
end
end
Autoextend.hook(:"Thor::Option", PatchThorWarning, method: :prepend)
# Extend any base classes, even gem classes
Dir.glob("#{Rails.root}/lib/ext/**/*.rb").sort.each { |file| require file }
# tell Rails to use the native XML parser instead of REXML
ActiveSupport::XmlMini.backend = 'Nokogiri'
class NotImplemented < StandardError; end
if defined?(PhusionPassenger)
PhusionPassenger.on_event(:after_installing_signal_handlers) do
Canvas::Reloader.trap_signal
end
else
config.to_prepare do
Canvas::Reloader.trap_signal
end
end
# Ensure that the automatic redis reconnection on fork works
# This is the default in redis-rb, but for some reason rails overrides it
# See e.g. https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22704
ActiveSupport::Cache::RedisCacheStore::DEFAULT_REDIS_OPTIONS[:reconnect_attempts] = 1
# don't wrap fields with errors with a <div class="fieldWithErrors" />,
# since that could leak information (e.g. valid vs invalid username on
# login page)
config.action_view.field_error_proc = proc { |html_tag, _instance| html_tag }
class ExceptionsApp
def call(env)
req = ActionDispatch::Request.new(env)
res = ApplicationController.make_response!(req)
ApplicationController.dispatch('rescue_action_dispatch_exception', req, res)
end
end
config.exceptions_app = ExceptionsApp.new
A new way of doing css/sass & New Canvas Theme Editor what this does: * Changes the way we generate css so we are able to generate custom css for people that use the theme editor. * Sets everything up so we can push all of our static assets (js, fonts, css, images, etc) to s3 pre-deploy and serve them from cloudfront. Yay! faster canvas for everyone! * as part of that, this enables the rails asset pipeline just so we can use it to put md5s in our urls. we don't use it for any of the coffeescript/sass/sprockets transformer stuff. * adds a new "Theme editor" functionality (only for people that have have the use-new-styles feature flag turned on) where an admin for an account can pick their own colors/images for all the users at their account/school. * when the user is done saving things in theme editor, it will, in a delayed job, generate all the css with against the variables that user specified and push it to s3 so it will be available to anyone else that requests it. (the delayed job will shell out to a node.js executable called `brandable_css`). * ability to pick an existing shared theme and to reset to blank theme. closes: CNVS-19685 * gets rid of jammit. test plan: (this is exaustive, so not every person has to do every step but we should make sure at least someone does each of these things. maybe as part of the review add a comment if you have done one of these bulletpoints) * before you check this out, compile all css and copy the public/stylsheets_compiled directory somewhere. after you check out this code and regenerate all the css. make sure there are no significant changes to the css output. (we updated the versions of node-sass and autoprefixer that we use so we want to make sure they don't change things in a way we weren't expecting) * make sure the way we load css for handlebars templates still works. eg: if there is a handlebars template at app/views/jst/some/template.handlebars if there is also a scss file at app/stylesheets/jst/some/template.scss then that stylesheet should get loaded when that template is rendered * check out the code and run migrations. browse around canvas, make sure css and js files load correctly as before. * cody, jacob, or someone on queso: look at the db migrations and make sure everything looks good and that I am handling sharding correctly. * verify that both rake canvas:compile_assets and guard, works as well as `node_modules/.bin/brandable_css` (note: if you have "node_modules/.bin" in your PATH (which you should), it will also work with just `brandable_css`) * verify that passing the --watch option to `.bin/node_modules/brandable_css` works and picks up changes to sass files, images, fonts, or any other resource that goes into a css file. and that it only recompiles the css files that actually depend on that file. * go to https://github.com/ryankshaw/brandable_css and check out the code there. that is what is actually doing the sass compiling * create a config/canvas_cdn.yml file and add aws access creds and an s3 bucket and cdn hostname (for testing, you can use the credentials for instructure_uploads_engineering from https://gollum.instructure.com/OtherServiceTestAccounts ). for a test cdn hostname you can use https://diu0rq5m1weh1.cloudfront.net. that is a cloudfront bucket I set up on my personal account that points to instructure_uploads_engineering * run rake canvas:compile_assets again, this time, at the end, you should see it run the assets:precompile task that puts md5s in filenames and, gzipps them, and copys them to public/assets. then you should see it run canvas:cdn:upload_to_s3 (look at log/development.log for progress), which pushes everything to s3. closes: CNVS-17333 CNVS-17430 CNVS-17337 * try out the theme editor: turn on new styles, go to accounts/x (where x is the @domain root acount you are testing from) and click the "theme editor" button on the right side of the page. that should take you to a page that has the ability to pick colors/images on the left side and preview your changes in an iframe on the right closes: CNVS-19360 CNVS-20551 * test the "preview", "save", "reset", and "choose existing" functionality closes: CNVS-17339 CNVS-17338 CNVS-19685 * make sure that the themeeditor works both if you have config/canvas_cdn.yml set up and enabled as well as if you don't. if it is enabled, you should see it push the css for just that new brand config to s3 when you hit preview, and the css should be accessible from the cdn you configured. Change-Id: Ie0a812d04f5eeb40e7df7e71941ff63ea51a4d22 Reviewed-on: https://gerrit.instructure.com/53873 Tested-by: Jenkins QA-Review: Jeremy Putnam <jeremyp@instructure.com> Reviewed-by: Jacob Fugal <jacob@instructure.com> Product-Review: Ryan Shaw <ryan@instructure.com>
2015-02-12 03:51:05 +08:00
config.before_initialize do
config.action_controller.asset_host = Canvas::Cdn.method(:asset_host_for)
A new way of doing css/sass & New Canvas Theme Editor what this does: * Changes the way we generate css so we are able to generate custom css for people that use the theme editor. * Sets everything up so we can push all of our static assets (js, fonts, css, images, etc) to s3 pre-deploy and serve them from cloudfront. Yay! faster canvas for everyone! * as part of that, this enables the rails asset pipeline just so we can use it to put md5s in our urls. we don't use it for any of the coffeescript/sass/sprockets transformer stuff. * adds a new "Theme editor" functionality (only for people that have have the use-new-styles feature flag turned on) where an admin for an account can pick their own colors/images for all the users at their account/school. * when the user is done saving things in theme editor, it will, in a delayed job, generate all the css with against the variables that user specified and push it to s3 so it will be available to anyone else that requests it. (the delayed job will shell out to a node.js executable called `brandable_css`). * ability to pick an existing shared theme and to reset to blank theme. closes: CNVS-19685 * gets rid of jammit. test plan: (this is exaustive, so not every person has to do every step but we should make sure at least someone does each of these things. maybe as part of the review add a comment if you have done one of these bulletpoints) * before you check this out, compile all css and copy the public/stylsheets_compiled directory somewhere. after you check out this code and regenerate all the css. make sure there are no significant changes to the css output. (we updated the versions of node-sass and autoprefixer that we use so we want to make sure they don't change things in a way we weren't expecting) * make sure the way we load css for handlebars templates still works. eg: if there is a handlebars template at app/views/jst/some/template.handlebars if there is also a scss file at app/stylesheets/jst/some/template.scss then that stylesheet should get loaded when that template is rendered * check out the code and run migrations. browse around canvas, make sure css and js files load correctly as before. * cody, jacob, or someone on queso: look at the db migrations and make sure everything looks good and that I am handling sharding correctly. * verify that both rake canvas:compile_assets and guard, works as well as `node_modules/.bin/brandable_css` (note: if you have "node_modules/.bin" in your PATH (which you should), it will also work with just `brandable_css`) * verify that passing the --watch option to `.bin/node_modules/brandable_css` works and picks up changes to sass files, images, fonts, or any other resource that goes into a css file. and that it only recompiles the css files that actually depend on that file. * go to https://github.com/ryankshaw/brandable_css and check out the code there. that is what is actually doing the sass compiling * create a config/canvas_cdn.yml file and add aws access creds and an s3 bucket and cdn hostname (for testing, you can use the credentials for instructure_uploads_engineering from https://gollum.instructure.com/OtherServiceTestAccounts ). for a test cdn hostname you can use https://diu0rq5m1weh1.cloudfront.net. that is a cloudfront bucket I set up on my personal account that points to instructure_uploads_engineering * run rake canvas:compile_assets again, this time, at the end, you should see it run the assets:precompile task that puts md5s in filenames and, gzipps them, and copys them to public/assets. then you should see it run canvas:cdn:upload_to_s3 (look at log/development.log for progress), which pushes everything to s3. closes: CNVS-17333 CNVS-17430 CNVS-17337 * try out the theme editor: turn on new styles, go to accounts/x (where x is the @domain root acount you are testing from) and click the "theme editor" button on the right side of the page. that should take you to a page that has the ability to pick colors/images on the left side and preview your changes in an iframe on the right closes: CNVS-19360 CNVS-20551 * test the "preview", "save", "reset", and "choose existing" functionality closes: CNVS-17339 CNVS-17338 CNVS-19685 * make sure that the themeeditor works both if you have config/canvas_cdn.yml set up and enabled as well as if you don't. if it is enabled, you should see it push the css for just that new brand config to s3 when you hit preview, and the css should be accessible from the cdn you configured. Change-Id: Ie0a812d04f5eeb40e7df7e71941ff63ea51a4d22 Reviewed-on: https://gerrit.instructure.com/53873 Tested-by: Jenkins QA-Review: Jeremy Putnam <jeremyp@instructure.com> Reviewed-by: Jacob Fugal <jacob@instructure.com> Product-Review: Ryan Shaw <ryan@instructure.com>
2015-02-12 03:51:05 +08:00
end
if config.action_dispatch.rack_cache != false
config.action_dispatch.rack_cache[:ignore_headers] =
%w[Set-Cookie X-Request-Context-Id X-Canvas-User-Id X-Canvas-Meta]
end
def validate_secret_key_base(_)
# no validation; we don't use Rails' CookieStore session middleware, so we
# don't care about secret_key_base
end
class DummyKeyGenerator
def self.generate_key(*)
end
end
def key_generator
DummyKeyGenerator
end
initializer "canvas.init_dynamic_settings", before: "canvas.extend_shard" do
settings = ConfigFile.load("consul")
if settings.present?
# this is not just for speed in non-consul installations^
# We also do things like building javascript assets with the base
# container that only has as many ruby assets as strictly necessary,
# and these resources actually aren't even on disk in those cases.
# do not remove this conditional until the asset build no longer
# needs the rails app for anything.
require_dependency 'canvas/dynamic_settings'
Canvas::DynamicSettingsInitializer.bootstrap!
end
end
initializer "canvas.cache_config", before: "canvas.extend_shard" do
CanvasCacheInit.apply!
end
initializer "canvas.extend_shard", before: "active_record.initialize_database" do
# have to do this before the default shard loads
Switchman::Shard.serialize :settings, Hash
Switchman.cache = -> { MultiCache.cache }
end
# Newer rails has this in rails proper
attr_writer :credentials
initializer "canvas.init_credentials", before: "active_record.initialize_database" do
self.credentials = Canvas::Credentials.new(credentials)
# Ensure we load credentials at initailization time to avoid overloading vault
self.credentials.config
end
# we don't know what middleware to make SessionsTimeout follow until after
# 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, 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
initializer("set_allowed_request_id_setters", after: :finisher_hook) do |app|
# apparently there is no initialization hook that comes late enough for
# routes to already be loaded, so we have to load them explicitly
app.reload_routes!
RequestContext::Generator.allow_unsigned_request_context_for(
app.routes.url_helpers.api_graphql_subgraph_path
)
end
end
end