mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Adds support for configuring HTTP Feature Policy (#33439)
A HTTP feature policy is Yet Another HTTP header for instructing the browser about which features the application intends to make use of and to lock down access to others. This is a new security mechanism that ensures that should an application become compromised or a third party attempts an unexpected action, the browser will override it and maintain the intended UX. WICG specification: https://wicg.github.io/feature-policy/ The end result is a HTTP header that looks like the following: ``` Feature-Policy: geolocation 'none'; autoplay https://example.com ``` This will prevent the browser from using geolocation and only allow autoplay on `https://example.com`. Full feature list can be found over in the WICG repository[1]. As of today Chrome and Safari have public support[2] for this functionality with Firefox working on support[3] and Edge still pending acceptance of the suggestion[4]. #### Examples Using an initializer ```rb # config/initializers/feature_policy.rb Rails.application.config.feature_policy do |f| f.geolocation :none f.camera :none f.payment "https://secure.example.com" f.fullscreen :self end ``` In a controller ```rb class SampleController < ApplicationController def index feature_policy do |f| f.geolocation "https://example.com" end end end ``` Some of you might realise that the HTTP feature policy looks pretty close to that of a Content Security Policy; and you're right. So much so that I used the Content Security Policy DSL from #31162 as the starting point for this change. This change *doesn't* introduce support for defining a feature policy on an iframe and this has been intentionally done to split the HTTP header and the HTML element (`iframe`) support. If this is successful, I'll look to add that on it's own. Full documentation on HTTP feature policies can be found at https://wicg.github.io/feature-policy/. Google have also published[5] a great in-depth write up of this functionality. [1]: https://github.com/WICG/feature-policy/blob/master/features.md [2]: https://www.chromestatus.com/feature/5694225681219584 [3]: https://bugzilla.mozilla.org/show_bug.cgi?id=1390801 [4]: https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/33507907-support-feature-policy [5]: https://developers.google.com/web/updates/2018/06/feature-policy
This commit is contained in:
parent
2fa21fe736
commit
bf19b8774e
14 changed files with 608 additions and 1 deletions
|
@ -1,3 +1,36 @@
|
|||
* Add DSL for configuring HTTP Feature Policy
|
||||
|
||||
This new DSL provides a way to configure a HTTP Feature Policy at a
|
||||
global or per-controller level. Full details of HTTP Feature Policy
|
||||
specification and guidelines can be found at MDN:
|
||||
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
|
||||
|
||||
Example global policy
|
||||
|
||||
```
|
||||
Rails.application.config.feature_policy do |f|
|
||||
f.camera :none
|
||||
f.gyroscope :none
|
||||
f.microphone :none
|
||||
f.usb :none
|
||||
f.fullscreen :self
|
||||
f.payment :self, "https://secure-example.com"
|
||||
end
|
||||
```
|
||||
|
||||
Example controller level policy
|
||||
|
||||
```
|
||||
class PagesController < ApplicationController
|
||||
feature_policy do |p|
|
||||
p.geolocation "https://example.com"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
*Jacob Bednarz*
|
||||
|
||||
* Add the ability to set the CSP nonce only to the specified directives.
|
||||
|
||||
Fixes #35137.
|
||||
|
|
|
@ -28,6 +28,7 @@ module ActionController
|
|||
autoload :DefaultHeaders
|
||||
autoload :EtagWithTemplateDigest
|
||||
autoload :EtagWithFlash
|
||||
autoload :FeaturePolicy
|
||||
autoload :Flash
|
||||
autoload :ForceSSL
|
||||
autoload :Head
|
||||
|
|
|
@ -226,6 +226,7 @@ module ActionController
|
|||
FormBuilder,
|
||||
RequestForgeryProtection,
|
||||
ContentSecurityPolicy,
|
||||
FeaturePolicy,
|
||||
ForceSSL,
|
||||
Streaming,
|
||||
DataStreaming,
|
||||
|
|
46
actionpack/lib/action_controller/metal/feature_policy.rb
Normal file
46
actionpack/lib/action_controller/metal/feature_policy.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActionController #:nodoc:
|
||||
# HTTP Feature Policy is a web standard for defining a mechanism to
|
||||
# allow and deny the use of browser features in its own context, and
|
||||
# in content within any <iframe> elements in the document.
|
||||
#
|
||||
# Full details of HTTP Feature Policy specification and guidelines can
|
||||
# be found at MDN:
|
||||
#
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
|
||||
#
|
||||
# Examples of usage:
|
||||
#
|
||||
# # Global policy
|
||||
# Rails.application.config.feature_policy do |f|
|
||||
# f.camera :none
|
||||
# f.gyroscope :none
|
||||
# f.microphone :none
|
||||
# f.usb :none
|
||||
# f.fullscreen :self
|
||||
# f.payment :self, "https://secure-example.com"
|
||||
# end
|
||||
#
|
||||
# # Controller level policy
|
||||
# class PagesController < ApplicationController
|
||||
# feature_policy do |p|
|
||||
# p.geolocation "https://example.com"
|
||||
# end
|
||||
# end
|
||||
module FeaturePolicy
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
module ClassMethods
|
||||
def feature_policy(**options, &block)
|
||||
before_action(options) do
|
||||
if block_given?
|
||||
policy = request.feature_policy.clone
|
||||
yield policy
|
||||
request.feature_policy = policy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -43,6 +43,7 @@ module ActionDispatch
|
|||
eager_autoload do
|
||||
autoload_under "http" do
|
||||
autoload :ContentSecurityPolicy
|
||||
autoload :FeaturePolicy
|
||||
autoload :Request
|
||||
autoload :Response
|
||||
end
|
||||
|
|
168
actionpack/lib/action_dispatch/http/feature_policy.rb
Normal file
168
actionpack/lib/action_dispatch/http/feature_policy.rb
Normal file
|
@ -0,0 +1,168 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/core_ext/object/deep_dup"
|
||||
|
||||
module ActionDispatch #:nodoc:
|
||||
class FeaturePolicy
|
||||
class Middleware
|
||||
CONTENT_TYPE = "Content-Type"
|
||||
POLICY = "Feature-Policy"
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
request = ActionDispatch::Request.new(env)
|
||||
_, headers, _ = response = @app.call(env)
|
||||
|
||||
return response unless html_response?(headers)
|
||||
return response if policy_present?(headers)
|
||||
|
||||
if policy = request.feature_policy
|
||||
headers[POLICY] = policy.build(request.controller_instance)
|
||||
end
|
||||
|
||||
if policy_empty?(policy)
|
||||
headers.delete(POLICY)
|
||||
end
|
||||
|
||||
response
|
||||
end
|
||||
|
||||
private
|
||||
def html_response?(headers)
|
||||
if content_type = headers[CONTENT_TYPE]
|
||||
content_type =~ /html/
|
||||
end
|
||||
end
|
||||
|
||||
def policy_present?(headers)
|
||||
headers[POLICY]
|
||||
end
|
||||
|
||||
def policy_empty?(policy)
|
||||
policy.try(:directives) && policy.directives.empty?
|
||||
end
|
||||
end
|
||||
|
||||
module Request
|
||||
POLICY = "action_dispatch.feature_policy"
|
||||
|
||||
def feature_policy
|
||||
get_header(POLICY)
|
||||
end
|
||||
|
||||
def feature_policy=(policy)
|
||||
set_header(POLICY, policy)
|
||||
end
|
||||
end
|
||||
|
||||
MAPPINGS = {
|
||||
self: "'self'",
|
||||
none: "'none'",
|
||||
}.freeze
|
||||
|
||||
# List of available features can be found at
|
||||
# https://github.com/WICG/feature-policy/blob/master/features.md#policy-controlled-features
|
||||
DIRECTIVES = {
|
||||
accelerometer: "accelerometer",
|
||||
ambient_light_sensor: "ambient-light-sensor",
|
||||
autoplay: "autoplay",
|
||||
camera: "camera",
|
||||
encrypted_media: "encrypted-media",
|
||||
fullscreen: "fullscreen",
|
||||
geolocation: "geolocation",
|
||||
gyroscope: "gyroscope",
|
||||
magnetometer: "magnetometer",
|
||||
microphone: "microphone",
|
||||
midi: "midi",
|
||||
payment: "payment",
|
||||
picture_in_picture: "picture-in-picture",
|
||||
speaker: "speaker",
|
||||
usb: "usb",
|
||||
vibrate: "vibrate",
|
||||
vr: "vr",
|
||||
}.freeze
|
||||
|
||||
private_constant :MAPPINGS, :DIRECTIVES
|
||||
|
||||
attr_reader :directives
|
||||
|
||||
def initialize
|
||||
@directives = {}
|
||||
yield self if block_given?
|
||||
end
|
||||
|
||||
def initialize_copy(other)
|
||||
@directives = other.directives.deep_dup
|
||||
end
|
||||
|
||||
DIRECTIVES.each do |name, directive|
|
||||
define_method(name) do |*sources|
|
||||
if sources.first
|
||||
@directives[directive] = apply_mappings(sources)
|
||||
else
|
||||
@directives.delete(directive)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build(context = nil)
|
||||
build_directives(context).compact.join("; ")
|
||||
end
|
||||
|
||||
private
|
||||
def apply_mappings(sources)
|
||||
sources.map do |source|
|
||||
case source
|
||||
when Symbol
|
||||
apply_mapping(source)
|
||||
when String, Proc
|
||||
source
|
||||
else
|
||||
raise ArgumentError, "Invalid HTTP feature policy source: #{source.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def apply_mapping(source)
|
||||
MAPPINGS.fetch(source) do
|
||||
raise ArgumentError, "Unknown HTTP feature policy source mapping: #{source.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def build_directives(context)
|
||||
@directives.map do |directive, sources|
|
||||
if sources.is_a?(Array)
|
||||
"#{directive} #{build_directive(sources, context).join(' ')}"
|
||||
elsif sources
|
||||
directive
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_directive(sources, context)
|
||||
sources.map { |source| resolve_source(source, context) }
|
||||
end
|
||||
|
||||
def resolve_source(source, context)
|
||||
case source
|
||||
when String
|
||||
source
|
||||
when Symbol
|
||||
source.to_s
|
||||
when Proc
|
||||
if context.nil?
|
||||
raise RuntimeError, "Missing context for the dynamic feature policy source: #{source.inspect}"
|
||||
else
|
||||
context.instance_exec(&source)
|
||||
end
|
||||
else
|
||||
raise RuntimeError, "Unexpected feature policy source: #{source.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -23,6 +23,7 @@ module ActionDispatch
|
|||
include ActionDispatch::Http::FilterParameters
|
||||
include ActionDispatch::Http::URL
|
||||
include ActionDispatch::ContentSecurityPolicy::Request
|
||||
include ActionDispatch::FeaturePolicy::Request
|
||||
include Rack::Request::Env
|
||||
|
||||
autoload :Session, "action_dispatch/request/session"
|
||||
|
|
142
actionpack/test/dispatch/feature_policy_test.rb
Normal file
142
actionpack/test/dispatch/feature_policy_test.rb
Normal file
|
@ -0,0 +1,142 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "abstract_unit"
|
||||
|
||||
class FeaturePolicyTest < ActiveSupport::TestCase
|
||||
def setup
|
||||
@policy = ActionDispatch::FeaturePolicy.new
|
||||
end
|
||||
|
||||
def test_mappings
|
||||
@policy.midi :self
|
||||
assert_equal "midi 'self'", @policy.build
|
||||
|
||||
@policy.midi :none
|
||||
assert_equal "midi 'none'", @policy.build
|
||||
end
|
||||
|
||||
def test_multiple_sources_for_a_single_directive
|
||||
@policy.geolocation :self, "https://example.com"
|
||||
assert_equal "geolocation 'self' https://example.com", @policy.build
|
||||
end
|
||||
|
||||
def test_single_directive_for_multiple_directives
|
||||
@policy.geolocation :self
|
||||
@policy.usb :none
|
||||
assert_equal "geolocation 'self'; usb 'none'", @policy.build
|
||||
end
|
||||
|
||||
def test_multiple_directives_for_multiple_directives
|
||||
@policy.geolocation :self, "https://example.com"
|
||||
@policy.usb :none, "https://example.com"
|
||||
assert_equal "geolocation 'self' https://example.com; usb 'none' https://example.com", @policy.build
|
||||
end
|
||||
|
||||
def test_invalid_directive_source
|
||||
exception = assert_raises(ArgumentError) do
|
||||
@policy.vr [:non_existent]
|
||||
end
|
||||
|
||||
assert_equal "Invalid HTTP feature policy source: [:non_existent]", exception.message
|
||||
end
|
||||
end
|
||||
|
||||
class FeaturePolicyIntegrationTest < ActionDispatch::IntegrationTest
|
||||
class PolicyController < ActionController::Base
|
||||
feature_policy only: :index do |f|
|
||||
f.gyroscope :none
|
||||
end
|
||||
|
||||
feature_policy only: :sample_controller do |f|
|
||||
f.gyroscope nil
|
||||
f.usb :self
|
||||
end
|
||||
|
||||
feature_policy only: :multiple_directives do |f|
|
||||
f.gyroscope nil
|
||||
f.usb :self
|
||||
f.autoplay "https://example.com"
|
||||
f.payment "https://secure.example.com"
|
||||
end
|
||||
|
||||
def index
|
||||
head :ok
|
||||
end
|
||||
|
||||
def sample_controller
|
||||
head :ok
|
||||
end
|
||||
|
||||
def multiple_directives
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
|
||||
ROUTES = ActionDispatch::Routing::RouteSet.new
|
||||
ROUTES.draw do
|
||||
scope module: "feature_policy_integration_test" do
|
||||
get "/", to: "policy#index"
|
||||
get "/sample_controller", to: "policy#sample_controller"
|
||||
get "/multiple_directives", to: "policy#multiple_directives"
|
||||
end
|
||||
end
|
||||
|
||||
POLICY = ActionDispatch::FeaturePolicy.new do |p|
|
||||
p.gyroscope :self
|
||||
end
|
||||
|
||||
class PolicyConfigMiddleware
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env["action_dispatch.feature_policy"] = POLICY
|
||||
env["action_dispatch.show_exceptions"] = false
|
||||
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
|
||||
APP = build_app(ROUTES) do |middleware|
|
||||
middleware.use PolicyConfigMiddleware
|
||||
middleware.use ActionDispatch::FeaturePolicy::Middleware
|
||||
end
|
||||
|
||||
def app
|
||||
APP
|
||||
end
|
||||
|
||||
def test_generates_feature_policy_header
|
||||
get "/"
|
||||
assert_policy "gyroscope 'none'"
|
||||
end
|
||||
|
||||
def test_generates_per_controller_feature_policy_header
|
||||
get "/sample_controller"
|
||||
assert_policy "usb 'self'"
|
||||
end
|
||||
|
||||
def test_generates_multiple_directives_feature_policy_header
|
||||
get "/multiple_directives"
|
||||
assert_policy "usb 'self'; autoplay https://example.com; payment https://secure.example.com"
|
||||
end
|
||||
|
||||
private
|
||||
def env_config
|
||||
Rails.application.env_config
|
||||
end
|
||||
|
||||
def feature_policy
|
||||
env_config["action_dispatch.feature_policy"]
|
||||
end
|
||||
|
||||
def feature_policy=(policy)
|
||||
env_config["action_dispatch.feature_policy"] = policy
|
||||
end
|
||||
|
||||
def assert_policy(expected)
|
||||
assert_response :success
|
||||
assert_equal expected, response.headers["Feature-Policy"]
|
||||
end
|
||||
end
|
|
@ -271,7 +271,8 @@ module Rails
|
|||
"action_dispatch.content_security_policy" => config.content_security_policy,
|
||||
"action_dispatch.content_security_policy_report_only" => config.content_security_policy_report_only,
|
||||
"action_dispatch.content_security_policy_nonce_generator" => config.content_security_policy_nonce_generator,
|
||||
"action_dispatch.content_security_policy_nonce_directives" => config.content_security_policy_nonce_directives
|
||||
"action_dispatch.content_security_policy_nonce_directives" => config.content_security_policy_nonce_directives,
|
||||
"action_dispatch.feature_policy" => config.feature_policy,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -69,6 +69,7 @@ module Rails
|
|||
@autoloader = :classic
|
||||
@disable_sandbox = false
|
||||
@add_autoload_paths_to_load_path = true
|
||||
@feature_policy = nil
|
||||
end
|
||||
|
||||
def load_defaults(target_version)
|
||||
|
@ -301,6 +302,14 @@ module Rails
|
|||
end
|
||||
end
|
||||
|
||||
def feature_policy(&block)
|
||||
if block_given?
|
||||
@feature_policy = ActionDispatch::FeaturePolicy.new(&block)
|
||||
else
|
||||
@feature_policy
|
||||
end
|
||||
end
|
||||
|
||||
def autoloader=(autoloader)
|
||||
case autoloader
|
||||
when :classic
|
||||
|
|
|
@ -68,6 +68,7 @@ module Rails
|
|||
|
||||
unless config.api_only
|
||||
middleware.use ::ActionDispatch::ContentSecurityPolicy::Middleware
|
||||
middleware.use ::ActionDispatch::FeaturePolicy::Middleware
|
||||
end
|
||||
|
||||
middleware.use ::Rack::Head
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# Define an application-wide HTTP feature policy. For further
|
||||
# information see https://developers.google.com/web/updates/2018/06/feature-policy
|
||||
#
|
||||
# Rails.application.config.feature_policy do |f|
|
||||
# f.camera :none
|
||||
# f.gyroscope :none
|
||||
# f.microphone :none
|
||||
# f.usb :none
|
||||
# f.fullscreen :self
|
||||
# f.payment :self, "https://secure-example.com"
|
||||
# end
|
191
railties/test/application/feature_policy_test.rb
Normal file
191
railties/test/application/feature_policy_test.rb
Normal file
|
@ -0,0 +1,191 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "isolation/abstract_unit"
|
||||
require "rack/test"
|
||||
|
||||
module ApplicationTests
|
||||
class FeaturePolicyTest < ActiveSupport::TestCase
|
||||
include ActiveSupport::Testing::Isolation
|
||||
include Rack::Test::Methods
|
||||
|
||||
def setup
|
||||
build_app
|
||||
end
|
||||
|
||||
def teardown
|
||||
teardown_app
|
||||
end
|
||||
|
||||
test "feature policy is not enabled by default" do
|
||||
controller :pages, <<-RUBY
|
||||
class PagesController < ApplicationController
|
||||
def index
|
||||
render html: "<h1>Welcome to Rails!</h1>"
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
app_file "config/routes.rb", <<-RUBY
|
||||
Rails.application.routes.draw do
|
||||
root to: "pages#index"
|
||||
end
|
||||
RUBY
|
||||
|
||||
app("development")
|
||||
|
||||
get "/"
|
||||
assert_nil last_response.headers["Feature-Policy"]
|
||||
end
|
||||
|
||||
test "global feature policy in an initializer" do
|
||||
controller :pages, <<-RUBY
|
||||
class PagesController < ApplicationController
|
||||
def index
|
||||
render html: "<h1>Welcome to Rails!</h1>"
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
app_file "config/initializers/feature_policy.rb", <<-RUBY
|
||||
Rails.application.config.feature_policy do |p|
|
||||
p.geolocation :none
|
||||
end
|
||||
RUBY
|
||||
|
||||
app_file "config/routes.rb", <<-RUBY
|
||||
Rails.application.routes.draw do
|
||||
root to: "pages#index"
|
||||
end
|
||||
RUBY
|
||||
|
||||
app("development")
|
||||
|
||||
get "/"
|
||||
assert_policy "geolocation 'none'"
|
||||
end
|
||||
|
||||
test "override feature policy using same directive in a controller" do
|
||||
controller :pages, <<-RUBY
|
||||
class PagesController < ApplicationController
|
||||
feature_policy do |p|
|
||||
p.geolocation "https://example.com"
|
||||
end
|
||||
|
||||
def index
|
||||
render html: "<h1>Welcome to Rails!</h1>"
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
app_file "config/initializers/feature_policy.rb", <<-RUBY
|
||||
Rails.application.config.feature_policy do |p|
|
||||
p.geolocation :none
|
||||
end
|
||||
RUBY
|
||||
|
||||
app_file "config/routes.rb", <<-RUBY
|
||||
Rails.application.routes.draw do
|
||||
root to: "pages#index"
|
||||
end
|
||||
RUBY
|
||||
|
||||
app("development")
|
||||
|
||||
get "/"
|
||||
assert_policy "geolocation https://example.com"
|
||||
end
|
||||
|
||||
test "override feature policy by unsetting a directive in a controller" do
|
||||
controller :pages, <<-RUBY
|
||||
class PagesController < ApplicationController
|
||||
feature_policy do |p|
|
||||
p.geolocation nil
|
||||
end
|
||||
|
||||
def index
|
||||
render html: "<h1>Welcome to Rails!</h1>"
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
app_file "config/initializers/feature_policy.rb", <<-RUBY
|
||||
Rails.application.config.feature_policy do |p|
|
||||
p.geolocation :none
|
||||
end
|
||||
RUBY
|
||||
|
||||
app_file "config/routes.rb", <<-RUBY
|
||||
Rails.application.routes.draw do
|
||||
root to: "pages#index"
|
||||
end
|
||||
RUBY
|
||||
|
||||
app("development")
|
||||
|
||||
get "/"
|
||||
assert_equal 200, last_response.status
|
||||
assert_nil last_response.headers["Feature-Policy"]
|
||||
end
|
||||
|
||||
test "override feature policy using different directives in a controller" do
|
||||
controller :pages, <<-RUBY
|
||||
class PagesController < ApplicationController
|
||||
feature_policy do |p|
|
||||
p.geolocation nil
|
||||
p.payment "https://secure.example.com"
|
||||
p.autoplay :none
|
||||
end
|
||||
|
||||
def index
|
||||
render html: "<h1>Welcome to Rails!</h1>"
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
app_file "config/initializers/feature_policy.rb", <<-RUBY
|
||||
Rails.application.config.feature_policy do |p|
|
||||
p.geolocation :none
|
||||
end
|
||||
RUBY
|
||||
|
||||
app_file "config/routes.rb", <<-RUBY
|
||||
Rails.application.routes.draw do
|
||||
root to: "pages#index"
|
||||
end
|
||||
RUBY
|
||||
|
||||
app("development")
|
||||
|
||||
get "/"
|
||||
assert_policy "payment https://secure.example.com; autoplay 'none'"
|
||||
end
|
||||
|
||||
test "global feature policy added to rack app" do
|
||||
app_file "config/initializers/feature_policy.rb", <<-RUBY
|
||||
Rails.application.config.feature_policy do |p|
|
||||
p.payment :none
|
||||
end
|
||||
RUBY
|
||||
|
||||
app_file "config/routes.rb", <<-RUBY
|
||||
Rails.application.routes.draw do
|
||||
app = ->(env) {
|
||||
[200, { "Content-Type" => "text/html" }, ["<p>Hello, World!</p>"]]
|
||||
}
|
||||
root to: app
|
||||
end
|
||||
RUBY
|
||||
|
||||
app("development")
|
||||
|
||||
get "/"
|
||||
assert_policy "payment 'none'"
|
||||
end
|
||||
|
||||
private
|
||||
def assert_policy(expected)
|
||||
assert_equal 200, last_response.status
|
||||
assert_equal expected, last_response.headers["Feature-Policy"]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -46,6 +46,7 @@ module ApplicationTests
|
|||
"ActionDispatch::Session::CookieStore",
|
||||
"ActionDispatch::Flash",
|
||||
"ActionDispatch::ContentSecurityPolicy::Middleware",
|
||||
"ActionDispatch::FeaturePolicy::Middleware",
|
||||
"Rack::Head",
|
||||
"Rack::ConditionalGet",
|
||||
"Rack::ETag",
|
||||
|
|
Loading…
Reference in a new issue