Skip to content

Commit ba09201

Browse files
jlledomclaude
andcommitted
Load CSP from a config file.
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent fc2aada commit ba09201

File tree

9 files changed

+194
-27
lines changed

9 files changed

+194
-27
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ config/backend_redis.yml
3232
config/cache_store.yml
3333
config/core.yml
3434
config/cors.yml
35+
/config/content_security_policy.yml
3536
config/currencies.yml
3637
config/database.yml
3738
config/domain_substitution.yml

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ C:\\nppdf32Log\\debuglog.txt
5252
/config/cache_store.yml
5353
/config/core.yml
5454
/config/cors.yml
55+
/config/content_security_policy.yml
5556
/config/currencies.yml
5657
/config/database.yml
5758
/config/domain_substitution.yml

config/application.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,10 @@ def cache_store_config
256256
config.three_scale.cors.enabled = false
257257
config.three_scale.cors.merge!(try_config_for(:cors) || {})
258258

259+
config.three_scale.content_security_policy = ActiveSupport::OrderedOptions.new
260+
config.three_scale.content_security_policy.enabled = false
261+
config.three_scale.content_security_policy.merge!(try_config_for(:content_security_policy) || {})
262+
259263
three_scale = config_for(:settings)
260264

261265
three_scale[:error_reporting_stages] = three_scale[:error_reporting_stages].to_s.split(/\W+/)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
base: &default
2+
enabled: true
3+
report_only: false
4+
5+
# Admin portal policy - restrictive but allows unsafe-inline/eval for existing code
6+
# Dynamically includes RAILS_ASSET_HOST for CDN assets if configured
7+
admin_portal_policy:
8+
default_src: ["'self'"]
9+
script_src: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "<%= ENV['RAILS_ASSET_HOST'] %>"]
10+
style_src: ["'self'", "'unsafe-inline'", "<%= ENV['RAILS_ASSET_HOST'] %>"]
11+
font_src: ["'self'", "data:", "<%= ENV['RAILS_ASSET_HOST'] %>"]
12+
img_src: ["'self'", "data:", "blob:", "https:"]
13+
connect_src: ["'self'"]
14+
frame_src: ["'self'"]
15+
frame_ancestors: ["'none'"]
16+
object_src: ["'none'"]
17+
base_uri: ["'self'"]
18+
19+
# Developer portal policy - permissive defaults for customization
20+
developer_portal_policy:
21+
default_src: [ "*", "data:", "mediastream:", "blob:", "filesystem:", "ws:", "wss:", "'unsafe-eval'", "'unsafe-inline'" ]
22+
23+
development:
24+
<<: *default
25+
enabled: true
26+
27+
test:
28+
<<: *default
29+
enabled: true
30+
31+
production:
32+
<<: *default
33+
enabled: true
Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
1-
# Be sure to restart your server when you modify this file.
1+
# frozen_string_literal: true
22

3-
# Define an application-wide content security policy.
4-
# See the Securing Rails Applications Guide for more information:
5-
# https://guides.rubyonrails.org/security.html#content-security-policy-header
3+
# Configure Content Security Policy headers
4+
# See: https://guides.rubyonrails.org/security.html#content-security-policy-header
65

7-
Rails.application.config.to_prepare do
8-
Rails.application.config.content_security_policy do |policy|
9-
policy.default_src '*', :data, :mediastream, :blob, :filesystem, :ws, :wss, :unsafe_eval, :unsafe_inline
6+
require_dependency 'three_scale/content_security_policy'
7+
8+
if ThreeScale::ContentSecurityPolicy::AdminPortal.enabled?
9+
# Apply configurable CSP from YAML
10+
Rails.application.configure do
11+
# Set report-only mode if configured
12+
config.content_security_policy_report_only = true if ThreeScale::ContentSecurityPolicy::AdminPortal.report_only?
1013
end
11-
end
1214

13-
# Rails.application.configure do
14-
# config.content_security_policy do |policy|
15-
# policy.default_src :self, :https
16-
# policy.font_src :self, :https, :data
17-
# policy.img_src :self, :https, :data
18-
# policy.object_src :none
19-
# policy.script_src :self, :https
20-
# policy.style_src :self, :https
21-
# # Specify URI for violation reports
22-
# # policy.report_uri "/csp-violation-report-endpoint"
23-
# end
24-
#
25-
# # Generate session nonces for permitted importmap, inline scripts, and inline styles.
26-
# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
27-
# config.content_security_policy_nonce_directives = %w(script-src style-src)
28-
#
29-
# # Report violations without enforcing the policy.
30-
# # config.content_security_policy_report_only = true
31-
# end
15+
# Apply global CSP policy from configuration
16+
Rails.application.config.to_prepare do
17+
policy_config = ThreeScale::ContentSecurityPolicy::AdminPortal.policy_config
18+
19+
if policy_config.present?
20+
Rails.application.config.content_security_policy do |policy|
21+
ThreeScale::ContentSecurityPolicy::AdminPortal.add_policy_config(policy, policy_config)
22+
end
23+
end
24+
end
25+
else
26+
# Fallback to permissive policy when config is disabled
27+
Rails.application.config.to_prepare do
28+
Rails.application.config.content_security_policy do |policy|
29+
policy.default_src '*', :data, :mediastream, :blob, :filesystem, :ws, :wss, :unsafe_eval, :unsafe_inline
30+
end
31+
end
32+
end

config/initializers/inflections.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
inflect.acronym 'GitHub'
2424
inflect.acronym 'SSO'
2525
inflect.acronym 'OIDC' # OpenID Connect
26+
inflect.acronym 'CSP' # Content Security Policy
2627
end
2728

2829
# These inflection rules are supported but not enabled by default:

lib/developer_portal/lib/developer_portal/engine.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
require 'three_scale/middleware/developer_portal_csp'
4+
15
module DeveloperPortal
26
class Engine < ::Rails::Engine
37
isolate_namespace DeveloperPortal
48

59
config.autoload_paths += %W(#{config.root.join('lib')})
610
config.paths.add 'lib', eager_load: true
711

12+
# Apply Developer Portal specific CSP policy
13+
config.middleware.use ThreeScale::Middleware::DeveloperPortalCSP
14+
815
initializer :assets do |config|
916
Rails.application.config.assets.precompile += %w{ stats.css }
1017
Rails.application.config.assets.precompile += %w{ stats.js }
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
module ThreeScale
4+
module Middleware
5+
class DeveloperPortalCSP
6+
def initialize(app)
7+
@app = app
8+
9+
# Pre-compute the CSP header once at startup since we don't use nonces or dynamic sources
10+
@csp_header_name, @csp_header_value = compute_csp_header
11+
end
12+
13+
def call(env)
14+
request = ActionDispatch::Request.new(env)
15+
16+
# We want to apply CSP only for HTML requests. However, we can't just return
17+
# because Rails will add global CSP policy (admin portal policy) to the response
18+
# if we don't do anything. We disable CSP for this request to prevent Rails middleware
19+
# to interfere.
20+
unless request.format.html?
21+
request.content_security_policy = false
22+
return @app.call(env)
23+
end
24+
25+
status, headers, _body = response = @app.call(env)
26+
27+
# Don't apply CSP to 304 responses to avoid cache issues
28+
return response if status == 304
29+
30+
# Only apply if we have a pre-computed CSP header
31+
headers[@csp_header_name] = @csp_header_value if @csp_header_value
32+
33+
response
34+
end
35+
36+
private
37+
38+
def compute_csp_header
39+
# Only compute if enabled and there's a policy configured
40+
policy_config = ThreeScale::ContentSecurityPolicy::DeveloperPortal.policy_config
41+
return [nil, nil] unless ThreeScale::ContentSecurityPolicy::DeveloperPortal.enabled? && policy_config.present?
42+
43+
# Build the policy once at initialization
44+
policy = ThreeScale::ContentSecurityPolicy::DeveloperPortal.build_policy(policy_config)
45+
header_name = if ThreeScale::ContentSecurityPolicy::DeveloperPortal.report_only?
46+
ActionDispatch::Constants::CONTENT_SECURITY_POLICY_REPORT_ONLY
47+
else
48+
ActionDispatch::Constants::CONTENT_SECURITY_POLICY
49+
end
50+
header_value = policy.build
51+
52+
[header_name, header_value]
53+
end
54+
end
55+
end
56+
end
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# frozen_string_literal: true
2+
3+
module ThreeScale
4+
module ContentSecurityPolicy
5+
class Base
6+
class << self
7+
def config
8+
@config ||= Rails.configuration.three_scale.content_security_policy
9+
end
10+
11+
def enabled?
12+
config&.enabled == true
13+
end
14+
15+
def policy_config
16+
raise NoMethodError, "#{__method__} not implemented in #{self.class}"
17+
end
18+
19+
def report_only?
20+
config&.report_only == true
21+
end
22+
23+
# Builds an ActionDispatch::ContentSecurityPolicy object from a policy configuration hash
24+
def build_policy(policy_config)
25+
ActionDispatch::ContentSecurityPolicy.new do |policy|
26+
add_policy_config(policy, policy_config)
27+
end
28+
end
29+
30+
# Applies a policy configuration hash to an existing policy object
31+
def add_policy_config(policy, policy_config)
32+
policy_config.each do |directive, values|
33+
method_name = directive.to_s
34+
next unless policy.respond_to?(method_name)
35+
36+
# Handle directives with sources (arrays) vs boolean directives
37+
if values.is_a?(Array)
38+
policy.public_send(method_name, *values)
39+
else
40+
policy.public_send(method_name, values)
41+
end
42+
end
43+
end
44+
end
45+
end
46+
47+
class AdminPortal < Base
48+
class << self
49+
def policy_config
50+
config&.admin_portal_policy&.to_h || {}
51+
end
52+
end
53+
end
54+
55+
class DeveloperPortal < Base
56+
class << self
57+
def policy_config
58+
config&.developer_portal_policy&.to_h || {}
59+
end
60+
end
61+
end
62+
end
63+
end

0 commit comments

Comments
 (0)