diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8777124c0..2f359a147 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
- with:
+ with:
submodules: false
- name: Setup git submodules
run: |
@@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
- with:
+ with:
submodules: false
- name: Setup git submodules
run: |
@@ -47,7 +47,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
- with:
+ with:
submodules: false
- name: Setup git submodules
run: |
@@ -65,7 +65,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
- with:
+ with:
submodules: false
- name: Setup git submodules
run: |
@@ -83,7 +83,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
- with:
+ with:
submodules: false
- name: Setup git submodules
run: |
@@ -101,7 +101,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
- with:
+ with:
submodules: false
- name: Setup git submodules
run: |
@@ -112,4 +112,4 @@ jobs:
- name: Setup database & test env
run: docker compose run web bin/rake db:create db:schema:load RAILS_ENV=test
- name: Run spec_integration tests
- run: docker compose run web bin/spec_integration
\ No newline at end of file
+ run: docker compose run web bin/spec_integration
diff --git a/Gemfile b/Gemfile
index 7b413d3cb..5b82c5eef 100644
--- a/Gemfile
+++ b/Gemfile
@@ -13,9 +13,9 @@ gem 'bootstrap', '~> 4.6' # Held back to 4.6
gem 'cancancan', '~> 3.6', '>= 3.6.1'
gem 'doorkeeper', '~> 5.7', '>= 5.7.1'
gem 'doorkeeper-openid_connect', '~> 1.8', '>= 1.8.9'
+gem 'faraday', '~> 2.14'
gem 'file_validators', '~> 3.0' # Used to validate organization logo
gem 'font-awesome-rails', '~> 4.7', '>= 4.7.0.8'
-gem 'google_drive', '~> 3.0', '>= 3.0.7', require: false
gem 'gravtastic', '~> 3.2', '>= 3.2.6' # Used to display user avatars
gem 'image_processing', '~> 1.12', '>= 1.12.2' # Used by active_storage to make logo 100x100 on the fly
gem 'logstasher', '~> 2.1', '>= 2.1.5'
diff --git a/Gemfile.lock b/Gemfile.lock
index 8f3137e0e..91fee7c49 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -130,7 +130,6 @@ GEM
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.4)
- declarative (0.0.20)
diff-lcs (1.5.1)
docile (1.4.1)
domain_name (0.6.20240107)
@@ -148,29 +147,12 @@ GEM
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
- faraday (1.10.3)
- faraday-em_http (~> 1.0)
- faraday-em_synchrony (~> 1.0)
- faraday-excon (~> 1.1)
- faraday-httpclient (~> 1.0)
- faraday-multipart (~> 1.0)
- faraday-net_http (~> 1.0)
- faraday-net_http_persistent (~> 1.0)
- faraday-patron (~> 1.0)
- faraday-rack (~> 1.0)
- faraday-retry (~> 1.0)
- ruby2_keywords (>= 0.0.4)
- faraday-em_http (1.0.0)
- faraday-em_synchrony (1.0.0)
- faraday-excon (1.1.0)
- faraday-httpclient (1.0.1)
- faraday-multipart (1.0.4)
- multipart-post (~> 2)
- faraday-net_http (1.0.2)
- faraday-net_http_persistent (1.2.0)
- faraday-patron (1.0.0)
- faraday-rack (1.0.0)
- faraday-retry (1.0.3)
+ faraday (2.14.0)
+ faraday-net_http (>= 2.0, < 3.5)
+ json
+ logger
+ faraday-net_http (3.4.2)
+ net-http (~> 0.5)
ffi (1.17.0)
ffi-compiler (1.3.2)
ffi (>= 1.15.5)
@@ -182,36 +164,11 @@ GEM
railties (>= 3.2, < 8.0)
globalid (1.2.1)
activesupport (>= 6.1)
- google-apis-core (0.11.3)
- addressable (~> 2.5, >= 2.5.1)
- googleauth (>= 0.16.2, < 2.a)
- httpclient (>= 2.8.1, < 3.a)
- mini_mime (~> 1.0)
- representable (~> 3.0)
- retriable (>= 2.0, < 4.a)
- rexml
- google-apis-drive_v3 (0.46.0)
- google-apis-core (>= 0.11.0, < 2.a)
- google-apis-sheets_v4 (0.26.0)
- google-apis-core (>= 0.11.0, < 2.a)
- google_drive (3.0.7)
- google-apis-drive_v3 (>= 0.5.0, < 1.0.0)
- google-apis-sheets_v4 (>= 0.4.0, < 1.0.0)
- googleauth (>= 0.5.0, < 1.0.0)
- nokogiri (>= 1.5.3, < 2.0.0)
- googleauth (0.17.1)
- faraday (>= 0.17.3, < 2.0)
- jwt (>= 1.4, < 3.0)
- memoist (~> 0.16)
- multi_json (~> 1.11)
- os (>= 0.9, < 2.0)
- signet (~> 0.15)
gravtastic (3.2.6)
hiredis (0.6.3)
http-accept (1.7.0)
http-cookie (1.0.6)
domain_name (~> 0.5)
- httpclient (2.8.3)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
image_processing (1.13.0)
@@ -250,7 +207,6 @@ GEM
net-smtp
marcel (1.0.4)
matrix (0.4.2)
- memoist (0.16.2)
method_source (1.1.0)
mime-types (3.5.2)
mime-types-data (~> 3.2015)
@@ -265,11 +221,12 @@ GEM
mini_mime (1.1.5)
minitest (5.24.1)
multi_json (1.15.0)
- multipart-post (2.4.1)
mustermann (3.0.0)
ruby2_keywords (~> 0.0.1)
mutex_m (0.2.0)
natcmp (1.4.3)
+ net-http (0.8.0)
+ uri (>= 0.11.1)
net-imap (0.4.14)
date
net-protocol
@@ -294,7 +251,6 @@ GEM
racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux)
racc (~> 1.4)
- os (1.1.4)
parallel (1.25.1)
parser (3.3.4.0)
ast (~> 2.4.1)
@@ -386,10 +342,6 @@ GEM
regexp_parser (2.9.2)
reline (0.5.9)
io-console (~> 0.5)
- representable (3.2.0)
- declarative (< 0.1.0)
- trailblazer-option (>= 0.1.1, < 0.2.0)
- uber (< 0.2.0)
request_store (1.7.0)
rack (>= 1.4)
responders (3.1.1)
@@ -400,7 +352,6 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
- retriable (3.1.2)
rexml (3.3.2)
strscan
rspec (3.13.0)
@@ -478,11 +429,6 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
- signet (0.19.0)
- addressable (~> 2.8)
- faraday (>= 0.17.5, < 3.a)
- jwt (>= 1.5, < 3.0)
- multi_json (~> 1.10)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
@@ -511,13 +457,12 @@ GEM
thor (1.3.1)
tilt (2.4.0)
timeout (0.4.1)
- trailblazer-option (0.1.2)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
- uber (0.1.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unicode-display_width (2.5.0)
+ uri (1.1.1)
webrick (1.8.1)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
@@ -557,9 +502,9 @@ DEPENDENCIES
doorkeeper (~> 5.7, >= 5.7.1)
doorkeeper-openid_connect (~> 1.8, >= 1.8.9)
factory_bot_rails (~> 6.4, >= 6.4.3)
+ faraday (~> 2.14)
file_validators (~> 3.0)
font-awesome-rails (~> 4.7, >= 4.7.0.8)
- google_drive (~> 3.0, >= 3.0.7)
gravtastic (~> 3.2, >= 3.2.6)
hiredis (~> 0.6.3)
image_processing (~> 1.12, >= 1.12.2)
diff --git a/app/controllers/api/v8/apidocs_controller.rb b/app/controllers/api/v8/apidocs_controller.rb
index e06e3e909..ff3dfc918 100644
--- a/app/controllers/api/v8/apidocs_controller.rb
+++ b/app/controllers/api/v8/apidocs_controller.rb
@@ -62,6 +62,13 @@ class ApidocsController < ActionController::Base
key :required, true
key :type, :integer
end
+ parameter :path_user_email do
+ key :name, :user_email
+ key :in, :path
+ key :description, "User's email"
+ key :required, true
+ key :type, :string
+ end
parameter :path_exercise_id do
key :name, :exercise_id
key :in, :path
diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb
index 88c129160..ec0357d57 100644
--- a/app/controllers/api/v8/users_controller.rb
+++ b/app/controllers/api/v8/users_controller.rb
@@ -56,6 +56,54 @@ class UsersController < Api::V8::BaseController
end
end
+ swagger_path '/api/v8/users/{user_id}/set_password_managed_by_courses_mooc_fi' do
+ operation :post do
+ key :description, 'Sets the boolean password_managed_by_courses_mooc_fi for the user with the given id to true.'
+ key :operationId, 'setPasswordManagedByCoursesMoocFi'
+ key :produces, ['application/json']
+ key :tags, ['user']
+ parameter '$ref': '#/parameters/user_id'
+ response 403, '$ref': '#/responses/error'
+ response 404, '$ref': '#/responses/error'
+ response 200 do
+ key :description, "status 'ok' and sets the boolean password_managed_by_courses_mooc_fi to true"
+ schema do
+ key :title, :status
+ key :required, [:status]
+ property :status, type: :string, example: 'Password managed by courses.mooc.fi set to true and password deleted.'
+ end
+ end
+ end
+ end
+
+ swagger_path '/api/v8/users/get_user_with_email?email={email}' do
+ operation :get do
+ key :description, "Returns the user's id as upstream_id, user's courses.mooc.fi-id as id, email, first name and last name by user email"
+ key :operationId, 'getUserInformationByEmail'
+ key :produces, ['application/json']
+ key :tags, ['user']
+ parameter '$ref': '#/parameters/user_email'
+ response 403, '$ref': '#/responses/error'
+ response 404, '$ref': '#/responses/error'
+ response 200 do
+ key :description, "User's courses.mooc.fi-id as id, email, first name, last name and id as upstream_id as json"
+ key :content, 'application/json'
+ schema do
+ key :title, :user
+ key :required, [:user]
+ key :type, :object
+ property :id, type: :string, description: "User's courses.mooc.fi user id", example: 'ABCD1234-5678-EFGH-IJ90-ABC123DEF456'
+ property :email, type: :string, description: 'User email', example: 'user@example.com'
+ property :first_name, type: :string, description: 'User first name', example: 'John'
+ property :last_name, type: :string, description: 'User last name', example: 'Doe'
+ property :upstream_id, type: :integer, description: "User's user id in TMC database", example: 123
+ end
+ end
+ end
+ end
+
+ skip_authorization_check only: %i[set_password_managed_by_courses_mooc_fi]
+
def show
unauthorize_guest! if current_user.guest?
user = current_user
@@ -103,19 +151,13 @@ def create
set_extra_data
if BannedEmail.banned?(@user.email)
- return render json: {
- success: true,
- message: 'User created.'
- }
+ return render json: build_success_response(params[:include_id])
end
if @user.errors.empty? && @user.save
# TODO: Whitelist origins
UserMailer.email_confirmation(@user, params[:origin], params[:language]).deliver_now
- render json: {
- success: true,
- message: 'User created.'
- }
+ render json: build_success_response(params[:include_id])
else
errors = @user.errors
errors[:username] = errors.delete(:login) if errors.key?(:login)
@@ -149,6 +191,67 @@ def update
}, status: :bad_request
end
+ def destroy
+ unauthorize_guest! if current_user.guest?
+
+ user = User.find(params[:id])
+ authorize! :destroy, user
+
+ User.transaction do
+ if user.destroy
+ RecentlyChangedUserDetail.where(username: user.login).delete_all
+ RecentlyChangedUserDetail.deleted.create!(
+ new_value: true,
+ email: user.email,
+ username: user.login,
+ user_id: user.id
+ )
+ Doorkeeper::AccessToken.where(resource_owner_id: user.id).delete_all
+
+ render json: { success: true, message: 'User deleted.' }
+ else
+ render json: { success: false, errors: user.errors }, status: :bad_request
+ end
+ end
+ end
+
+ def set_password_managed_by_courses_mooc_fi
+ only_admins!
+
+ User.transaction do
+ user = User.find_by!(id: params[:id])
+ user.password_managed_by_courses_mooc_fi = true
+ user.password_hash = nil
+ user.salt = nil
+ user.argon_hash = nil
+ user.courses_mooc_fi_user_id = params[:courses_mooc_fi_user_id]
+ raise ActiveRecord::Rollback if !user.errors.empty? || !user.save
+ return render json: {
+ status: 'Password managed by courses.mooc.fi set to true and password deleted.'
+ }
+ end
+ render json: {
+ errors: @user.errors
+ }, status: :bad_request
+ end
+
+ def get_user_with_email
+ unauthorize_guest! if current_user.guest?
+
+ user = User.find_by!(email: params[:email])
+ authorize! :read, user
+
+ name = UserFieldValue.where(user_id: user.id, field_name: ['first_name', 'last_name']).pluck(:field_name, :value).to_h
+
+ render json: {
+ id: user.courses_mooc_fi_user_id,
+ email: user.email,
+ first_name: name['first_name'],
+ last_name: name['last_name'],
+ upstream_id: user.id,
+ }
+ end
+
private
def set_email
user_params = params[:user]
@@ -189,6 +292,10 @@ def set_password
end
def maybe_update_password
+ if @user.password_managed_by_courses_mooc_fi && @user.courses_mooc_fi_user_id.present?
+ return @user.update_password_via_courses_mooc_fi(@user.courses_mooc_fi_user_id, user_params[:old_password], user_params[:password])
+ end
+
if params[:old_password].present? && params[:password].present?
if !@user.has_password?(params[:old_password])
@user.errors.add(:old_password, 'incorrect')
@@ -229,6 +336,17 @@ def set_extra_data(eager_save = false)
datum.save! if eager_save
end
end
+
+ def build_success_response(include_id = false)
+ response = {
+ success: true,
+ message: 'User created.',
+ }
+ if include_id
+ response[:id] = @user.id
+ end
+ response
+ end
end
end
end
diff --git a/app/controllers/password_reset_keys_controller.rb b/app/controllers/password_reset_keys_controller.rb
index 5ab403593..dfeff5906 100644
--- a/app/controllers/password_reset_keys_controller.rb
+++ b/app/controllers/password_reset_keys_controller.rb
@@ -39,18 +39,29 @@ def destroy
return render action: :show, status: :forbidden
end
- @user.password = params[:password]
- if @user.save
- @key.destroy
- flash[:success] = 'Your password has been reset.'
- redirect_to root_path
+ if @user.password_managed_by_courses_mooc_fi
+ success = @user.update_password_via_courses_mooc_fi(nil, params[:password])
+ if success
+ @key.destroy
+ flash[:success] = 'Your password has been reset.'
+ redirect_to root_path
+ else
+ 'Failed to reset password.'
+ end
else
- flash.now[:alert] = if @user.errors[:password]
- 'Password ' + @user.errors[:password].join(', ')
+ @user.password = params[:password]
+ if @user.save
+ @key.destroy
+ flash[:success] = 'Your password has been reset.'
+ redirect_to root_path
else
- 'Failed to set password'
+ flash.now[:alert] = if @user.errors[:password]
+ 'Password ' + @user.errors[:password].join(', ')
+ else
+ 'Failed to set password'
+ end
+ render action: :show, status: :forbidden
end
- render action: :show, status: :forbidden
end
end
diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb
index 1915ac4c9..21e8f2801 100644
--- a/app/controllers/settings_controller.rb
+++ b/app/controllers/settings_controller.rb
@@ -86,6 +86,10 @@ def set_email
end
def maybe_update_password(user, user_params)
+ if user.password_managed_by_courses_mooc_fi && user.courses_mooc_fi_user_id.present?
+ return user.update_password_via_courses_mooc_fi(user.courses_mooc_fi_user_id, user_params[:old_password], user_params[:password])
+ end
+
if user_params[:old_password].present? || user_params[:password].present?
if !user.has_password?(user_params[:old_password])
user.errors.add(:old_password, 'incorrect')
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index d7e2246d4..be012b747 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -179,6 +179,10 @@ def set_password
end
def maybe_update_password(user, user_params)
+ if user.password_managed_by_courses_mooc_fi && user.courses_mooc_fi_user_id.present?
+ return user.update_password_via_courses_mooc_fi(user.courses_mooc_fi_user_id, user_params[:old_password], user_params[:password])
+ end
+
if user_params[:old_password].present? || user_params[:password].present?
if !user.has_password?(user_params[:old_password])
user.errors.add(:old_password, 'incorrect')
diff --git a/app/helpers/points_helper.rb b/app/helpers/points_helper.rb
index fdc0ee595..91348bbee 100644
--- a/app/helpers/points_helper.rb
+++ b/app/helpers/points_helper.rb
@@ -9,19 +9,6 @@ def github_repo_url_to_project_page_url(url)
end
end
- def gdocs_notifications(notifications)
- ret = ""
- ret += notifications.map do |msg|
- if msg =~ /^error/ || msg =~ /^exception/
- "
'
- ret
- end
-
def points_list(points)
points.to_a.natsort.map { |pt| h(pt) }.join(' ').html_safe
end
diff --git a/app/models/course.rb b/app/models/course.rb
index 5669e13ae..e50a89ca2 100644
--- a/app/models/course.rb
+++ b/app/models/course.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
-require 'gdocs_export'
require 'system_commands'
require 'date_and_time_utils'
@@ -292,10 +291,6 @@ def gdocs_sheets(exercises = nil)
exercises.map(&:gdocs_sheet).reject(&:nil?).uniq
end
- def refresh_gdocs_worksheet(sheetname)
- GDocsExport.refresh_course_worksheet_points self, sheetname
- end
-
def self.cache_root
"#{FileStore.root}/course"
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 721e5c041..14b451fc2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -146,9 +146,101 @@ def self.authenticate(login, submitted_password)
user = find_by(login: login)
user ||= find_by('lower(email) = ?', login.downcase)
return nil if user.nil?
+
+ if user.password_managed_by_courses_mooc_fi && user.courses_mooc_fi_user_id.present?
+ return user if user.authenticate_via_courses_mooc_fi(submitted_password)
+ return nil
+ end
+
user if user.has_password?(submitted_password)
end
+
+ def authenticate_via_courses_mooc_fi(submitted_password)
+ auth_url = SiteSetting.value('courses_mooc_fi_auth_url')
+
+ conn = Faraday.new do |f|
+ f.request :json
+ f.response :json
+ end
+
+ response = conn.post(auth_url) do |req|
+ req.headers['Content-Type'] = 'application/json'
+ req.headers['Accept'] = 'application/json'
+ req.headers['Authorization'] = Base64.decode64(
+ Rails.application.secrets.tmc_server_secret_for_communicating_to_secret_project
+ )
+
+ req.body = {
+ user_id: courses_mooc_fi_user_id,
+ password: submitted_password
+ }
+ end
+
+ response.body == true
+
+ rescue Faraday::ClientError => e
+ status = e.response&.dig(:status)
+
+ if status == 401 || status == 403
+ return false
+ end
+
+ Rails.logger.error("Authentication via courses.mooc.fi error: #{e.response}")
+ raise
+
+ rescue => e
+ Rails.logger.error("Unexpected error during authentication via courses.mooc.fi: #{e.message}")
+ raise
+ end
+
+
+
+ def update_password_via_courses_mooc_fi(old_password, new_password)
+ update_url = SiteSetting.value('courses_mooc_fi_update_password_url')
+
+ conn = Faraday.new do |f|
+ f.request :json
+ f.response :json
+ end
+
+ begin
+ response = conn.post(update_url) do |req|
+ req.headers['Content-Type'] = 'application/json'
+ req.headers['Accept'] = 'application/json'
+ req.headers['Authorization'] = Base64.decode64(
+ Rails.application.secrets.tmc_server_secret_for_communicating_to_secret_project
+ )
+
+ req.body = {
+ user_id: self.courses_mooc_fi_user_id,
+ old_password: old_password,
+ new_password: new_password
+ }
+ end
+
+ data = response.body
+
+ unless data == true
+ raise "Updating password via courses.mooc.fi failed for user with courses.mooc.fi-user-id #{self.courses_mooc_fi_user_id}"
+ end
+
+ true
+
+ rescue Faraday::ClientError => e
+ Rails.logger.error(
+ "Updating password via courses.mooc.fi failed for user with courses.mooc.fi-user-id #{self.courses_mooc_fi_user_id}: #{e.response}"
+ )
+ false
+
+ rescue => e
+ Rails.logger.error(
+ "Unexpected error updating password via courses.mooc.fi for user with courses.mooc.fi-user-id #{self.courses_mooc_fi_user_id}: #{e.message}"
+ )
+ false
+ end
+ end
+
def password_reset_key
action_tokens.find { |t| t.action == 'reset_password' }
end
diff --git a/app/views/points/refresh_gdocs.html.erb b/app/views/points/refresh_gdocs.html.erb
deleted file mode 100644
index 6f9e66d26..000000000
--- a/app/views/points/refresh_gdocs.html.erb
+++ /dev/null
@@ -1,7 +0,0 @@
-