Skip to content

Commit effaeff

Browse files
fix(auth): fix overridden default host handling/redirect
1 parent df49860 commit effaeff

8 files changed

Lines changed: 150 additions & 170 deletions

File tree

app/controllers/concerns/application_multitenancy.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def deduce_tenant
1515
tenant_host = deduce_tenant_host
1616
instance = Instance.find_tenant_by_host_or_default(tenant_host)
1717

18-
if Rails.env.production? && instance.default? && instance.host.casecmp(tenant_host) != 0
18+
if instance.default? && tenant_host.casecmp(Instance.default.host) != 0
1919
raise ActionController::RoutingError, 'Instance Not Found'
2020
end
2121

@@ -25,16 +25,26 @@ def deduce_tenant
2525
# Deduces the current host. We strip any leading www from the host.
2626
# @return [String] The host, with www removed.
2727
def deduce_tenant_host
28-
stripped_host = request.host.downcase.start_with?('www.') ? request.host[4..] : request.host
29-
default_host = Application::Application.config.x.default_host&.gsub(/:\d+$/, '')
28+
stripped_host = normalize_tenant_host(request.host)
29+
default_host = normalize_tenant_host(Instance.default.host)
3030

31-
if default_host && stripped_host.downcase.ends_with?(default_host)
31+
if default_host && stripped_host == default_host
32+
Instance.default.host
33+
elsif default_host && stripped_host.ends_with?(default_host)
3234
stripped_host.sub(default_host, 'coursemology.org')
3335
else
3436
stripped_host
3537
end
3638
end
3739

40+
# We store "host" strings in database without port modifiers,
41+
# even when Coursemology is configured with them as part of the host (in local development).
42+
def normalize_tenant_host(host)
43+
return nil unless host
44+
45+
(host.starts_with?('www.') ? host[4..] : host).gsub(/:\d+$/, '').downcase
46+
end
47+
3848
module ClassMethods
3949
def set_current_tenant_through_filter
4050
super

app/models/instance.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def default?
133133

134134
# Replace the hostname of the default instance.
135135
def host
136-
default_host = Application::Application.config.x.default_host || ENV.fetch('RAILS_HOSTNAME', 'localhost:8080')
136+
default_host = Application::Application.config.x.default_host
137137
return default_host if default?
138138

139139
read_attribute(:host).gsub('coursemology.org', default_host)

client/app/api/ErrorHandling.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import { AxiosResponse } from 'axios';
22

3-
import {
4-
AUTH_USER_MANAGER,
5-
oidcConfig,
6-
} from 'lib/components/wrappers/AuthProvider';
3+
import { AUTH_USER_MANAGER } from 'lib/components/wrappers/AuthProvider';
74
import {
85
redirectToForbidden,
96
redirectToNotFound,
@@ -33,7 +30,7 @@ const isSuspendedResponse = (response?: AxiosResponse): boolean =>
3330

3431
export const redirectIfMatchesErrorIn = (response?: AxiosResponse): void => {
3532
if (isUnauthenticatedResponse(response))
36-
AUTH_USER_MANAGER.signinRedirect({ redirect_uri: oidcConfig.redirect_uri });
33+
AUTH_USER_MANAGER.signinRedirect({ redirect_uri: window.location.href });
3734
if (isSuspendedResponse(response)) redirectToSuspended();
3835
if (isUnauthorizedResponse(response))
3936
// Should open a new window and login

config/environments/production.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
# NGINX, varnish or squid.
2727
# config.action_dispatch.rack_cache = true
2828
# We will configure the host from the environment.
29-
config.action_mailer.default_url_options = { host: ENV['RAILS_HOSTNAME'] }
29+
config.action_mailer.default_url_options = { host: ENV.fetch('RAILS_HOSTNAME') }
3030

3131
# Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
3232
# or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
@@ -134,7 +134,7 @@
134134
# config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
135135
# config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
136136
#
137-
config.x.default_host = ENV['RAILS_HOSTNAME']
137+
config.x.default_host = ENV.fetch('RAILS_HOSTNAME')
138138
config.active_job.queue_adapter = :sidekiq
139139

140140
# Rails 6.0.5.1 security patch

spec/controllers/application_controller_spec.rb

Lines changed: 85 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def publicly_accessible?
1616
end
1717
end
1818

19+
let(:instance) { create(:instance) }
1920
describe ApplicationControllerMultitenancyConcern do
2021
# These variables override the hostname in the selected tenant object before with_tenant gets
2122
# to execute. This effectively changes the host requested in the mock request.
@@ -25,10 +26,8 @@ def publicly_accessible?
2526
with_tenant(:instance) do
2627
context 'when a nonexistent instance is specified' do
2728
let(:instance) { build_stubbed(:instance) }
28-
it 'falls back to the default instance' do
29-
get :index
30-
31-
expect(ActsAsTenant.current_tenant).to eq(Instance.default)
29+
it 'raises a RoutingError' do
30+
expect { get :index }.to raise_error(ActionController::RoutingError)
3231
end
3332
end
3433

@@ -54,11 +53,8 @@ def publicly_accessible?
5453

5554
context 'when the host has a subdomain other than www' do
5655
let(:instance_host) { "random.#{instance.host.upcase}" }
57-
it 'finds the actual host' do
58-
get :index
59-
60-
expect(ActsAsTenant.current_tenant).not_to eq(instance)
61-
expect(ActsAsTenant.current_tenant).to eq(Instance.default)
56+
it 'raises a RoutingError' do
57+
expect { get :index }.to raise_error(ActionController::RoutingError)
6258
end
6359
end
6460
end
@@ -69,8 +65,8 @@ def publicly_accessible?
6965
subject { controller.send(:deduce_tenant_host) }
7066

7167
before do
72-
allow(Application::Application.config.x).to receive(:default_host).
73-
and_return('staging.coursemology.org')
68+
allow(Instance).to receive(:default).
69+
and_return(instance_double(Instance, host: 'staging.coursemology.org'))
7470
end
7571

7672
context 'when the host has a www prefix' do
@@ -79,10 +75,10 @@ def publicly_accessible?
7975
it { is_expected.to eq('example.com') }
8076
end
8177

82-
context 'when the host matches the default host' do
78+
context 'when the host exactly matches the default host' do
8379
before { @request.host = 'staging.coursemology.org' }
8480

85-
it { is_expected.to eq('coursemology.org') }
81+
it { is_expected.to eq('staging.coursemology.org') }
8682
end
8783

8884
context 'when the host is a subdomain of the default host' do
@@ -105,82 +101,88 @@ def publicly_accessible?
105101
end
106102

107103
describe ApplicationInternationalizationConcern do
108-
before { @old_i18n_locale = I18n.locale }
109-
after { I18n.locale = @old_i18n_locale }
104+
with_tenant(:instance) do
105+
before { @old_i18n_locale = I18n.locale }
106+
after { I18n.locale = @old_i18n_locale }
110107

111-
pending 'when http accept language is present' do
112-
before { @request.env['HTTP_ACCEPT_LANGUAGE'] = 'zh-cn' }
108+
context 'when http accept language is present' do
109+
before { @request.env['HTTP_ACCEPT_LANGUAGE'] = 'zh' }
113110

114-
it 'sets the correct locale' do
115-
get :index
116-
expect(I18n.locale).to eq(:'zh-CN')
111+
it 'sets the correct locale' do
112+
get :index
113+
expect(I18n.locale).to eq(:zh)
114+
end
117115
end
118-
end
119116

120-
context 'when http accept language is not present' do
121-
before { @request.env['HTTP_ACCEPT_LANGUAGE'] = nil }
117+
context 'when http accept language is not present' do
118+
before { @request.env['HTTP_ACCEPT_LANGUAGE'] = nil }
122119

123-
it 'sets the locale to default' do
124-
get :index
125-
expect(I18n.locale).to eq(I18n.default_locale)
120+
it 'sets the locale to default' do
121+
get :index
122+
expect(I18n.locale).to eq(I18n.default_locale)
123+
end
126124
end
127-
end
128125

129-
context 'when http accept language is not available' do
130-
before do
131-
@request.env['HTTP_ACCEPT_LANGUAGE'] = 'jp'
132-
get :index
133-
end
126+
context 'when http accept language is not available' do
127+
before do
128+
@request.env['HTTP_ACCEPT_LANGUAGE'] = 'jp'
129+
get :index
130+
end
134131

135-
it 'sets the locale to default' do
136-
expect(I18n.locale).to eq(I18n.default_locale)
132+
it 'sets the locale to default' do
133+
expect(I18n.locale).to eq(I18n.default_locale)
134+
end
137135
end
138-
end
139136

140-
pending 'when http accept language belongs to the same region' do
141-
before { @request.env['HTTP_ACCEPT_LANGUAGE'] = 'zh-tw' }
137+
pending 'when http accept language belongs to the same region' do
138+
before { @request.env['HTTP_ACCEPT_LANGUAGE'] = 'zh-tw' }
142139

143-
it 'sets the nearest locale' do
144-
get :index
145-
expect(I18n.locale).to eq(:'zh-CN')
140+
it 'sets the nearest locale' do
141+
get :index
142+
expect(I18n.locale).to eq(:'zh-CN')
143+
end
146144
end
147-
end
148145

149-
pending 'when multiple http accept languages are present' do
150-
before { @request.env['HTTP_ACCEPT_LANGUAGE'] = 'jp, zh-tw' }
146+
pending 'when multiple http accept languages are present' do
147+
before { @request.env['HTTP_ACCEPT_LANGUAGE'] = 'jp, zh-tw' }
151148

152-
it 'sets the nearest available locale' do
153-
get :index
154-
expect(I18n.locale).to eq(:'zh-CN')
149+
it 'sets the nearest available locale' do
150+
get :index
151+
expect(I18n.locale).to eq(:'zh-CN')
152+
end
155153
end
156154
end
157155
end
158156

159157
describe ApplicationUserConcern do
160-
context 'when the action raises a CanCan::AccessDenied' do
161-
run_rescue
158+
with_tenant(:instance) do
159+
context 'when the action raises a CanCan::AccessDenied' do
160+
run_rescue
162161

163-
it 'returns HTTP status 403' do
164-
post :create
165-
expect(response.status).to eq(403)
162+
it 'returns HTTP status 403' do
163+
post :create
164+
expect(response.status).to eq(403)
165+
end
166166
end
167167
end
168168
end
169169

170170
describe ApplicationComponentsConcern do
171-
context 'when the action raises a Coursemology::ComponentNotFoundError' do
172-
run_rescue
171+
with_tenant(:instance) do
172+
context 'when the action raises a Coursemology::ComponentNotFoundError' do
173+
run_rescue
173174

174-
before do
175-
def controller.index
176-
raise ComponentNotFoundError
175+
before do
176+
def controller.index
177+
raise ComponentNotFoundError
178+
end
177179
end
178-
end
179180

180-
it 'returns HTTP status 404' do
181-
get :index
182-
expect(response.status).to eq(404)
183-
expect(response.body).to include('Component not found')
181+
it 'returns HTTP status 404' do
182+
get :index
183+
expect(response.status).to eq(404)
184+
expect(response.body).to include('Component not found')
185+
end
184186
end
185187
end
186188
end
@@ -194,34 +196,38 @@ def controller.index
194196
end
195197

196198
context 'when the action raises an IllegalStateError' do
197-
run_rescue
199+
with_tenant(:instance) do
200+
run_rescue
198201

199-
before do
200-
def controller.index
201-
raise IllegalStateError
202+
before do
203+
def controller.index
204+
raise IllegalStateError
205+
end
202206
end
203-
end
204207

205-
it 'returns HTTP status 422' do
206-
get :index
207-
expect(response.status).to eq(422)
208+
it 'returns HTTP status 422' do
209+
get :index
210+
expect(response.status).to eq(422)
211+
end
208212
end
209213
end
210214

211215
context 'when the action raises ActionController::InvalidAuthenticityToken' do
212-
run_rescue
216+
with_tenant(:instance) do
217+
run_rescue
213218

214-
before do
215-
def controller.index
216-
raise ActionController::InvalidAuthenticityToken
219+
before do
220+
def controller.index
221+
raise ActionController::InvalidAuthenticityToken
222+
end
217223
end
218-
end
219224

220-
it 'returns HTTP status 403' do
221-
# Replaced specific error check due to potential false positives
222-
# expect { get :index }.to_not raise_error ActionController::InvalidAuthenticityToken
223-
expect { get :index }.to_not raise_error
224-
expect(response.status).to eq(403)
225+
it 'returns HTTP status 403' do
226+
# Replaced specific error check due to potential false positives
227+
# expect { get :index }.to_not raise_error ActionController::InvalidAuthenticityToken
228+
expect { get :index }.to_not raise_error
229+
expect(response.status).to eq(403)
230+
end
225231
end
226232
end
227233
end

0 commit comments

Comments
 (0)