diff --git a/app/lib/content_security_policy.rb b/app/lib/content_security_policy.rb new file mode 100644 index 00000000000..e8fcf76a656 --- /dev/null +++ b/app/lib/content_security_policy.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +class ContentSecurityPolicy + def base_host + Rails.configuration.x.web_domain + end + + def assets_host + url_from_configured_asset_host || url_from_base_host + end + + def media_host + cdn_host_value || assets_host + end + + private + + def url_from_configured_asset_host + Rails.configuration.action_controller.asset_host + end + + def cdn_host_value + s3_alias_host || s3_cloudfront_host || azure_alias_host || s3_hostname_host + end + + def url_from_base_host + host_to_url(base_host) + end + + def host_to_url(host_string) + uri_from_configuration_and_string(host_string) if host_string.present? + end + + def s3_alias_host + host_to_url ENV.fetch('S3_ALIAS_HOST', nil) + end + + def s3_cloudfront_host + host_to_url ENV.fetch('S3_CLOUDFRONT_HOST', nil) + end + + def azure_alias_host + host_to_url ENV.fetch('AZURE_ALIAS_HOST', nil) + end + + def s3_hostname_host + host_to_url ENV.fetch('S3_HOSTNAME', nil) + end + + def uri_from_configuration_and_string(host_string) + Addressable::URI.parse("#{host_protocol}://#{host_string}").tap do |uri| + uri.path += '/' unless uri.path.blank? || uri.path.end_with?('/') + end.to_s + end + + def host_protocol + Rails.configuration.x.use_https ? 'https' : 'http' + end +end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index e5e672f2c77..8f8ea580288 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -6,24 +6,11 @@ # See the Securing Rails Applications Guide for more information: # https://guides.rubyonrails.org/security.html#content-security-policy-header -def host_to_url(str) - return if str.blank? +require_relative '../../app/lib/content_security_policy' - uri = Addressable::URI.parse("http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}") - uri.path += '/' unless uri.path.blank? || uri.path.end_with?('/') - uri.to_s -end - -base_host = Rails.configuration.x.web_domain - -assets_host = Rails.configuration.action_controller.asset_host -assets_host ||= host_to_url(base_host) - -media_host = host_to_url(ENV['S3_ALIAS_HOST']) -media_host ||= host_to_url(ENV['S3_CLOUDFRONT_HOST']) -media_host ||= host_to_url(ENV['AZURE_ALIAS_HOST']) -media_host ||= host_to_url(ENV['S3_HOSTNAME']) if ENV['S3_ENABLED'] == 'true' -media_host ||= assets_host +policy = ContentSecurityPolicy.new +assets_host = policy.assets_host +media_host = policy.media_host def sso_host return unless ENV['ONE_CLICK_SSO_LOGIN'] == 'true' diff --git a/spec/lib/content_security_policy_spec.rb b/spec/lib/content_security_policy_spec.rb new file mode 100644 index 00000000000..2e92f815acc --- /dev/null +++ b/spec/lib/content_security_policy_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe ContentSecurityPolicy do + subject { described_class.new } + + around do |example| + original_asset_host = Rails.configuration.action_controller.asset_host + original_web_domain = Rails.configuration.x.web_domain + original_use_https = Rails.configuration.x.use_https + example.run + Rails.configuration.action_controller.asset_host = original_asset_host + Rails.configuration.x.web_domain = original_web_domain + Rails.configuration.x.use_https = original_use_https + end + + describe '#base_host' do + before { Rails.configuration.x.web_domain = 'host.example' } + + it 'returns the configured value for the web domain' do + expect(subject.base_host).to eq 'host.example' + end + end + + describe '#assets_host' do + context 'when asset_host is not configured' do + before { Rails.configuration.action_controller.asset_host = nil } + + context 'with a configured web domain' do + before { Rails.configuration.x.web_domain = 'host.example' } + + context 'when use_https is enabled' do + before { Rails.configuration.x.use_https = true } + + it 'returns value from base host with https protocol' do + expect(subject.assets_host).to eq 'https://host.example' + end + end + + context 'when use_https is disabled' do + before { Rails.configuration.x.use_https = false } + + it 'returns value from base host with http protocol' do + expect(subject.assets_host).to eq 'http://host.example' + end + end + end + end + + context 'when asset_host is configured' do + before do + Rails.configuration.action_controller.asset_host = 'https://assets.host.example' + end + + it 'returns full value from configured host' do + expect(subject.assets_host).to eq 'https://assets.host.example' + end + end + end + + describe '#media_host' do + context 'when there is no configured CDN' do + it 'defaults to using the assets_host value' do + expect(subject.media_host).to eq(subject.assets_host) + end + end + + context 'when an S3 alias host is configured' do + around do |example| + ClimateControl.modify S3_ALIAS_HOST: 'asset-host.s3-alias.example' do + example.run + end + end + + it 'uses the s3 alias host value' do + expect(subject.media_host).to eq 'https://asset-host.s3-alias.example' + end + end + + context 'when an S3 alias host with a trailing path is configured' do + around do |example| + ClimateControl.modify S3_ALIAS_HOST: 'asset-host.s3-alias.example/pathname' do + example.run + end + end + + it 'uses the s3 alias host value and preserves the path' do + expect(subject.media_host).to eq 'https://asset-host.s3-alias.example/pathname/' + end + end + + context 'when an S3 cloudfront host is configured' do + around do |example| + ClimateControl.modify S3_CLOUDFRONT_HOST: 'asset-host.s3-cloudfront.example' do + example.run + end + end + + it 'uses the s3 cloudfront host value' do + expect(subject.media_host).to eq 'https://asset-host.s3-cloudfront.example' + end + end + + context 'when an azure alias host is configured' do + around do |example| + ClimateControl.modify AZURE_ALIAS_HOST: 'asset-host.azure-alias.example' do + example.run + end + end + + it 'uses the azure alias host value' do + expect(subject.media_host).to eq 'https://asset-host.azure-alias.example' + end + end + + context 'when s3_enabled is configured' do + around do |example| + ClimateControl.modify S3_ENABLED: 'true', S3_HOSTNAME: 'asset-host.s3.example' do + example.run + end + end + + it 'uses the s3 hostname host value' do + expect(subject.media_host).to eq 'https://asset-host.s3.example' + end + end + end +end