Skip to content

Commit a4c8388

Browse files
authored
Migrate existing users to courses.mooc.fi on login and password changes (#595)
1 parent 5a37854 commit a4c8388

9 files changed

Lines changed: 169 additions & 4 deletions

File tree

app/controllers/api/v8/users_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ def update
187187
update_email
188188
maybe_update_password
189189
raise ActiveRecord::Rollback if !@user.errors.empty? || !@user.save
190+
# Password changed locally: migrate the user to courses.mooc.fi
191+
if params[:old_password].present? && params[:password].present? && !@user.managed_externally?
192+
@user.post_new_user_to_courses_mooc_fi(params[:password])
193+
end
190194
RecentlyChangedUserDetail.email_changed.create!(old_value: @email_before, new_value: @user.email, username: @user.login, user_id: @user.id) unless @email_before.casecmp(@user.email).zero?
191195
return render json: {
192196
message: 'User details updated.'

app/controllers/password_reset_keys_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ def destroy
5858
else
5959
@user.password = params[:password]
6060
if @user.save
61+
# Not yet managed by courses.mooc.fi: migrate the user there with the new password
62+
@user.post_new_user_to_courses_mooc_fi(params[:password])
6163
@key.destroy
6264
flash[:success] = 'Your password has been reset.'
6365
redirect_to root_path

app/controllers/settings_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ def update
1919
set_user_fields
2020

2121
if @user.errors.empty? && @user.save
22+
# Password changed locally: migrate the user to courses.mooc.fi
23+
@user.post_new_user_to_courses_mooc_fi(params[:user][:password]) if password_changed && !@user.managed_externally?
2224
RecentlyChangedUserDetail.email_changed.create!(old_value: @email_before, new_value: @user.email, username: @user.login, user_id: @user.id) unless @email_before.casecmp(@user.email).zero?
2325
flash[:notice] = if password_changed
2426
'Changes saved and password changed'

app/controllers/users_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ def update
8282
set_user_fields
8383

8484
if @user.errors.empty? && @user.save
85+
# Password changed locally: migrate the user to courses.mooc.fi
86+
@user.post_new_user_to_courses_mooc_fi(params[:user][:password]) if password_changed && !@user.managed_externally?
8587
RecentlyChangedUserDetail.email_changed.create!(old_value: @email_before, new_value: @user.email, username: @user.login, user_id: @user.id) unless @email_before.casecmp(@user.email).zero?
8688
flash[:notice] = if password_changed
8789
'Changes saved and password changed'

app/models/user.rb

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,11 @@ def self.authenticate(login, submitted_password)
176176
return nil
177177
end
178178

179-
user if user.has_password?(submitted_password)
179+
if user.has_password?(submitted_password)
180+
# Locally-managed user logged in: migrate them to courses.mooc.fi
181+
user.post_new_user_to_courses_mooc_fi(submitted_password)
182+
user
183+
end
180184
end
181185

182186
# The password is stored in courses.mooc.fi and we have the id needed to delegate auth/changes.
@@ -194,7 +198,7 @@ def externally_managed_without_target?
194198
def authenticate_via_courses_mooc_fi(submitted_password)
195199
auth_url = SiteSetting.value('courses_mooc_fi_auth_url')
196200

197-
conn = Faraday.new do |f|
201+
conn = Faraday.new(request: { open_timeout: 2, timeout: 10 }) do |f|
198202
f.request :json
199203
f.response :json
200204
end
@@ -243,7 +247,7 @@ def authenticate_via_courses_mooc_fi(submitted_password)
243247
def update_password_via_courses_mooc_fi(old_password, new_password)
244248
update_url = SiteSetting.value('courses_mooc_fi_update_password_url')
245249

246-
conn = Faraday.new do |f|
250+
conn = Faraday.new(request: { open_timeout: 2, timeout: 10 }) do |f|
247251
f.request :json
248252
f.response :json
249253
end
@@ -299,7 +303,9 @@ def post_new_user_to_courses_mooc_fi(password)
299303
Rails.logger.info("Posting new user #{self.email} to courses.mooc.fi")
300304
create_url = SiteSetting.value('courses_mooc_fi_create_user_url')
301305

302-
conn = Faraday.new do |f|
306+
# Best-effort call made inline during logins/password changes: tight timeouts so a hung
307+
# courses.mooc.fi can't stall authentication (migration retries on the next attempt).
308+
conn = Faraday.new(request: { open_timeout: 2, timeout: 10 }) do |f|
303309
f.request :json
304310
f.response :json
305311
end

spec/controllers/api/v8/users_controller_spec.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,46 @@
121121
end
122122
end
123123
end
124+
125+
describe 'PUT update password' do
126+
let!(:token) { double resource_owner_id: user.id, acceptable?: true }
127+
128+
before :each do
129+
user.password = 'oldpassword'
130+
user.save!
131+
end
132+
133+
def do_update(old_password)
134+
put :update, params: { id: 'current', user: { email: user.email },
135+
old_password: old_password, password: 'newpassword', password_repeat: 'newpassword' }
136+
end
137+
138+
it 'migrates the user to courses.mooc.fi when the password is changed' do
139+
expect_any_instance_of(User).to receive(:post_new_user_to_courses_mooc_fi).with('newpassword').and_return(true)
140+
141+
do_update('oldpassword')
142+
143+
expect(response).to have_http_status(200)
144+
expect(user.reload).to have_password('newpassword')
145+
end
146+
147+
it 'does not migrate the user when the password change fails' do
148+
expect_any_instance_of(User).not_to receive(:post_new_user_to_courses_mooc_fi)
149+
150+
do_update('wrongpassword')
151+
152+
expect(response).to have_http_status(400)
153+
expect(user.reload).to have_password('oldpassword')
154+
end
155+
156+
it 'delegates to courses.mooc.fi without migrating for an already managed user' do
157+
user.update!(password_managed_by_courses_mooc_fi: true, courses_mooc_fi_user_id: SecureRandom.uuid)
158+
expect_any_instance_of(User).to receive(:update_password_via_courses_mooc_fi).with('oldpassword', 'newpassword').and_return(true)
159+
expect_any_instance_of(User).not_to receive(:post_new_user_to_courses_mooc_fi)
160+
161+
do_update('oldpassword')
162+
163+
expect(response).to have_http_status(200)
164+
end
165+
end
124166
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe PasswordResetKeysController, type: :controller do
6+
describe 'DELETE destroy' do
7+
before :each do
8+
@user = FactoryBot.create(:user)
9+
@key = ActionToken.generate_password_reset_key_for(@user)
10+
end
11+
12+
def do_destroy
13+
delete :destroy, params: { token: @key.token, password: 'new_password', password_confirmation: 'new_password' }
14+
end
15+
16+
describe 'for a user not yet managed by courses.mooc.fi' do
17+
it 'resets the local password and migrates the user to courses.mooc.fi' do
18+
expect_any_instance_of(User).to receive(:post_new_user_to_courses_mooc_fi).with('new_password').and_return(true)
19+
20+
do_destroy
21+
22+
expect(response).to redirect_to(root_path)
23+
expect(@user.reload).to have_password('new_password')
24+
expect(ActionToken.find_by(id: @key.id)).to be_nil
25+
end
26+
27+
it 'still resets the password when migration fails' do
28+
expect_any_instance_of(User).to receive(:post_new_user_to_courses_mooc_fi).with('new_password').and_return(false)
29+
30+
do_destroy
31+
32+
expect(response).to redirect_to(root_path)
33+
expect(@user.reload).to have_password('new_password')
34+
end
35+
end
36+
37+
describe 'for a user managed by courses.mooc.fi' do
38+
before :each do
39+
@user.update!(password_managed_by_courses_mooc_fi: true, courses_mooc_fi_user_id: SecureRandom.uuid)
40+
end
41+
42+
it 'delegates the reset to courses.mooc.fi without migrating' do
43+
expect_any_instance_of(User).to receive(:update_password_via_courses_mooc_fi).with(nil, 'new_password').and_return(true)
44+
expect_any_instance_of(User).not_to receive(:post_new_user_to_courses_mooc_fi)
45+
46+
do_destroy
47+
48+
expect(response).to redirect_to(root_path)
49+
expect(ActionToken.find_by(id: @key.id)).to be_nil
50+
end
51+
end
52+
end
53+
end

spec/controllers/users_controller_spec.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,32 @@
191191
expect(@user.reload).to have_password('newpassword')
192192
end
193193

194+
it 'should migrate the user to courses.mooc.fi when the password is changed' do
195+
expect_any_instance_of(User).to receive(:post_new_user_to_courses_mooc_fi).with('newpassword').and_return(true)
196+
put :update, params: { user: params.merge(old_password: 'oldpassword',
197+
password: 'newpassword',
198+
password_repeat: 'newpassword') }
199+
expect(response).to redirect_to(user_path)
200+
end
201+
202+
it 'should not migrate the user to courses.mooc.fi when the password change fails' do
203+
expect_any_instance_of(User).not_to receive(:post_new_user_to_courses_mooc_fi)
204+
put :update, params: { user: params.merge(old_password: 'wrongpassword',
205+
password: 'newpassword',
206+
password_repeat: 'newpassword') }
207+
expect(response.status).to eq(403)
208+
end
209+
210+
it 'should delegate to courses.mooc.fi without migrating for an already managed user' do
211+
@user.update!(password_managed_by_courses_mooc_fi: true, courses_mooc_fi_user_id: SecureRandom.uuid)
212+
expect_any_instance_of(User).to receive(:update_password_via_courses_mooc_fi).with('oldpassword', 'newpassword').and_return(true)
213+
expect_any_instance_of(User).not_to receive(:post_new_user_to_courses_mooc_fi)
214+
put :update, params: { user: params.merge(old_password: 'oldpassword',
215+
password: 'newpassword',
216+
password_repeat: 'newpassword') }
217+
expect(response).to redirect_to(user_path)
218+
end
219+
194220
it 'should not change the password if the old password was wrong' do
195221
put :update, params: { user: params.merge(old_password: 'wrongpassword',
196222
password: 'newpassword',

spec/models/user_spec.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,34 @@
270270
expect(User.authenticate('root', 'ilikecookies')).to be_nil
271271
end
272272

273+
describe 'migrating to courses.mooc.fi on login' do
274+
it 'posts a locally-managed user on successful authentication' do
275+
user = User.create!(login: 'localuser', password: 'secret123', email: 'localuser@example.com')
276+
expect_any_instance_of(User).to receive(:post_new_user_to_courses_mooc_fi).with('secret123').and_return(true)
277+
expect(User.authenticate('localuser', 'secret123')).to eq(user)
278+
end
279+
280+
it 'still authenticates when the migration post fails' do
281+
user = User.create!(login: 'localuser', password: 'secret123', email: 'localuser@example.com')
282+
expect_any_instance_of(User).to receive(:post_new_user_to_courses_mooc_fi).with('secret123').and_return(false)
283+
expect(User.authenticate('localuser', 'secret123')).to eq(user)
284+
end
285+
286+
it 'does not post when authentication fails' do
287+
User.create!(login: 'localuser', password: 'secret123', email: 'localuser@example.com')
288+
expect_any_instance_of(User).not_to receive(:post_new_user_to_courses_mooc_fi)
289+
expect(User.authenticate('localuser', 'wrongpassword')).to be_nil
290+
end
291+
292+
it 'does not post for an already managed user' do
293+
user = User.create!(login: 'manageduser', password: 'secret123', email: 'managed@example.com')
294+
user.update!(password_managed_by_courses_mooc_fi: true, courses_mooc_fi_user_id: SecureRandom.uuid)
295+
expect_any_instance_of(User).not_to receive(:post_new_user_to_courses_mooc_fi)
296+
allow_any_instance_of(User).to receive(:authenticate_via_courses_mooc_fi).with('secret123').and_return(true)
297+
expect(User.authenticate('manageduser', 'secret123')).to eq(user)
298+
end
299+
end
300+
273301
describe 'visibility' do
274302
before :each do
275303
@organization1 = FactoryBot.create :accepted_organization, slug: 'slug1'

0 commit comments

Comments
 (0)