Skip to content

Commit c0fd6b3

Browse files
authored
Allow for dynamic hosts in CORS origin config (#409)
## Status - Closes #405 ## What's changed? This extends the current origin-setting functionality (literal origins set via a comma-separated environment variable) and allows the env var string to also contain regular expressions. These allow the origins to match dynamic patterns (eg. `https://*.projects-ui.pages.dev`) and ensures that configuration still remains in the environment as we don't want to assume that anyone running this application will want the same allowed origins configured as us (other than for `localhost` for local and test environments only). eg. instead of currently having to set the following in `ALLOWED_ORIGINS`: ``` https://foo.raspberrypi.org, https://bar.raspberrypi.org, https://foo.editor-standalone-eyq.pages.dev, https://bar.editor-standalone-eyq.pages.dev ``` The following can be set: ``` /https:\/\/(?:[a-z0-9-]+\.)?raspberrypi\.org$/, /https:\/\/.+\.editor-standalone-eyq\.pages\.dev$/ ``` Associated updates in Terraform allow for the origins to match the spec in the [issue](#405) (eg. the addition of wildcards / regex matching): * RaspberryPiFoundation/terraform#898
1 parent 406c81a commit c0fd6b3

File tree

7 files changed

+92
-23
lines changed

7 files changed

+92
-23
lines changed

Diff for: .env.example

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
ALLOWED_ORIGINS=localhost:3000,localhost:3002,localhost:3009,localhost:3010,localhost:3012,editor.rpfdev.com
1+
# localhost is set as an origin by default in development (config/initializers/cors.rb)
2+
# so you probably only need to set ALLOWED_ORIGINS for debugging purposes
3+
ALLOWED_ORIGINS=""
24

35
AWS_ACCESS_KEY_ID=changeme
46
AWS_S3_ACTIVE_STORAGE_BUCKET=changeme

Diff for: README.md

+1-5
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,7 @@ docker-compose run api rspec spec/path/to/spec.rb
127127

128128
### CORS Allowed Origins
129129

130-
Add a comma separated list to the relevant enviroment settings. E.g for development in the `.env` file:
131-
132-
```
133-
ALLOWED_ORIGINS=localhost:3002,localhost:3000
134-
```
130+
Handled in `config/initializers/cors.rb`.
135131

136132
### Webhooks
137133

Diff for: config/initializers/cors.rb

+17-14
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
# frozen_string_literal: true
22

33
# Be sure to restart your server when you modify this file.
4+
# Read more: https://github.com/cyu/rack-cors
45

5-
origins_array = ENV['ALLOWED_ORIGINS']&.split(',')&.map(&:strip) || []
6-
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin Ajax requests.
6+
require Rails.root.join('lib/origin_parser')
77

88
Rails.application.config.middleware.insert_before 0, Rack::Cors do
99
allow do
10-
origins origins_array
11-
resource '*', headers: :any, methods: %i[get post patch put delete], expose: ['Link']
10+
# localhost and test domain origins
11+
origins(%r{https?://localhost([:0-9]*)$}) if Rails.env.development? || Rails.env.test?
12+
13+
standard_cors_options
14+
end
15+
16+
allow do
17+
# environment-specific origins set through ALLOWED_ORIGINS env var
18+
# should only be necessary for staging / production environments (see above for local and test)
19+
origins OriginParser.parse_origins
20+
21+
standard_cors_options
1222
end
1323
end
14-
# Read more: https://github.com/cyu/rack-cors
1524

16-
# Rails.application.config.middleware.insert_before 0, Rack::Cors do
17-
# allow do
18-
# origins "example.com"
19-
#
20-
# resource "*",
21-
# headers: :any,
22-
# methods: [:get, :post, :put, :patch, :delete, :options, :head]
23-
# end
24-
# end
25+
def standard_cors_options
26+
resource '*', headers: :any, methods: %i[get post patch put delete], expose: ['Link']
27+
end

Diff for: lib/corp_middleware.rb

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require_relative 'origin_parser'
4+
35
class CorpMiddleware
46
def initialize(app)
57
@app = app
@@ -8,9 +10,11 @@ def initialize(app)
810
def call(env)
911
status, headers, response = @app.call(env)
1012
request_origin = env['HTTP_HOST']
11-
allowed_origins = ENV['ALLOWED_ORIGINS']&.split(',')&.map(&:strip) || []
13+
allowed_origins = OriginParser.parse_origins
1214

13-
if env['PATH_INFO'].start_with?('/rails/active_storage') && allowed_origins.include?(request_origin)
15+
if env['PATH_INFO'].start_with?('/rails/active_storage') && allowed_origins.any? do |origin|
16+
origin.is_a?(Regexp) ? origin =~ request_origin : origin == request_origin
17+
end
1418
headers['Cross-Origin-Resource-Policy'] = 'cross-origin'
1519
end
1620

Diff for: lib/origin_parser.rb

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
# fetch origins from the environment in a comma-separated string
4+
# these can be literal strings or regexes
5+
# regexes must be wrapped in forward slashes eg. /https?:\/\/localhost(:[0-9]*)?$/
6+
module OriginParser
7+
def self.parse_origins
8+
ENV['ALLOWED_ORIGINS']&.split(',')&.map do |origin|
9+
stripped_origin = origin.strip
10+
if stripped_origin.start_with?('/') && stripped_origin.end_with?('/')
11+
Regexp.new(stripped_origin[1..-2])
12+
else
13+
stripped_origin
14+
end
15+
end || []
16+
end
17+
end

Diff for: spec/lib/corp_middleware_spec.rb

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,15 @@
1212
allow(ENV).to receive(:[]).with('ALLOWED_ORIGINS').and_return('test.com')
1313
end
1414

15-
it 'sets the Cross-Origin-Resource-Policy header for allowed origins' do
15+
it 'sets the Cross-Origin-Resource-Policy header for a literal origin' do
16+
_status, headers, _response = middleware.call(env)
17+
18+
expect(headers['Cross-Origin-Resource-Policy']).to eq('cross-origin')
19+
end
20+
21+
it 'sets the Cross-Origin-Resource-Policy header for regex origin' do
22+
allow(ENV).to receive(:[]).with('ALLOWED_ORIGINS').and_return('/test\.com/')
23+
1624
_status, headers, _response = middleware.call(env)
1725

1826
expect(headers['Cross-Origin-Resource-Policy']).to eq('cross-origin')

Diff for: spec/lib/origin_parser_spec.rb

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe OriginParser do
6+
describe '.parse_origins' do
7+
after { ENV['ALLOWED_ORIGINS'] = nil }
8+
9+
it 'returns an empty array if ALLOWED_ORIGINS is not set' do
10+
ENV['ALLOWED_ORIGINS'] = nil
11+
expect(described_class.parse_origins).to eq([])
12+
end
13+
14+
it 'parses literal strings correctly' do
15+
ENV['ALLOWED_ORIGINS'] = 'http://example.com, https://example.org'
16+
expect(described_class.parse_origins).to eq(['http://example.com', 'https://example.org'])
17+
end
18+
19+
it 'parses regexes correctly' do
20+
ENV['ALLOWED_ORIGINS'] = '/https?:\/\/example\.com/'
21+
expect(described_class.parse_origins).to eq([Regexp.new('https?:\/\/example\.com')])
22+
end
23+
24+
it 'parses a mix of literals and regexes' do
25+
ENV['ALLOWED_ORIGINS'] = 'http://example.com, /https?:\/\/localhost$/'
26+
expect(described_class.parse_origins).to eq(['http://example.com', Regexp.new('https?:\/\/localhost$')])
27+
end
28+
29+
it 'strips whitespace from origins' do
30+
ENV['ALLOWED_ORIGINS'] = ' http://example.com , /regex$/ '
31+
expect(described_class.parse_origins).to eq(['http://example.com', Regexp.new('regex$')])
32+
end
33+
34+
it 'returns an empty array if ALLOWED_ORIGINS is empty' do
35+
ENV['ALLOWED_ORIGINS'] = ''
36+
expect(described_class.parse_origins).to eq([])
37+
end
38+
end
39+
end

0 commit comments

Comments
 (0)