From b25a15558645398d5358dd5628186b275eb82fd0 Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Fri, 11 Jul 2025 00:36:02 +0300 Subject: [PATCH 01/21] Add api endpoint for setting user password to be handled by moocfi --- app/controllers/api/v8/users_controller.rb | 38 ++++ config/routes.rb | 1 + ...add_password_managed_by_moocfi_to_users.rb | 5 + db/schema.rb | 176 +++++++++--------- 4 files changed, 132 insertions(+), 88 deletions(-) create mode 100644 db/migrate/20250710194038_add_password_managed_by_moocfi_to_users.rb diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index 88c129160..4b28c9eba 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -56,6 +56,27 @@ class UsersController < Api::V8::BaseController end end + swagger_path '/api/v8/users/{user_id}/set_password_managed_by_moocfi' do + operation :post do + key :description, 'Sets the boolean password_managed_by_moocfi for the user with the given id to payload value.' + key :operationId, 'setPasswordManagedByMoocfi' + 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_moocfi" + schema do + key :title, :status + key :required, [:status] + property :status, type: :string, example: 'Password managed by Mooc.fi set to true.' + end + end + end + end + + def show unauthorize_guest! if current_user.guest? user = current_user @@ -149,6 +170,23 @@ def update }, status: :bad_request end + def set_password_managed_by_moocfi + unauthorize_guest! if current_user.guest? + + @user = User.find_by!(id: params[:id]) + authorize! :update, @user + @user.password_managed_by_moocfi = params[:set_password_managed_by_moocfi] + if @user.save + render json: { + status: "Password managed by Mooc.fi set to #{params[:set_password_managed_by_moocfi]}." + } + else + render json: { + errors: @user.errors + }, status: :bad_request + end + end + private def set_email user_params = params[:user] diff --git a/config/routes.rb b/config/routes.rb index f3c7c23b1..6daa54f56 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -59,6 +59,7 @@ resources :request_deletion, only: [:create], module: :users resources :assistantships, module: :users, only: :index resources :teacherships, module: :users, only: :index + post :set_password_managed_by_moocfi, on: :member end resources :user_app_datum, only: [:index] diff --git a/db/migrate/20250710194038_add_password_managed_by_moocfi_to_users.rb b/db/migrate/20250710194038_add_password_managed_by_moocfi_to_users.rb new file mode 100644 index 000000000..1e88db464 --- /dev/null +++ b/db/migrate/20250710194038_add_password_managed_by_moocfi_to_users.rb @@ -0,0 +1,5 @@ +class AddPasswordManagedByMoocfiToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :password_managed_by_moocfi, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 32be56cfd..e27f4f729 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,18 +10,17 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2024_03_04_085436) do - +ActiveRecord::Schema[7.1].define(version: 2025_07_10_194038) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "action_tokens", id: :serial, force: :cascade do |t| t.integer "user_id", null: false t.string "token", null: false - t.datetime "created_at", null: false + t.datetime "created_at", precision: nil, null: false t.integer "action", null: false - t.datetime "expires_at" - t.datetime "updated_at" + t.datetime "expires_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["user_id"], name: "index_action_tokens_on_user_id" end @@ -30,7 +29,7 @@ t.string "record_type", null: false t.bigint "record_id", null: false t.bigint "blob_id", null: false - t.datetime "created_at", null: false + t.datetime "created_at", precision: nil, null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end @@ -43,7 +42,7 @@ t.string "service_name", null: false t.bigint "byte_size", null: false t.string "checksum", null: false - t.datetime "created_at", null: false + t.datetime "created_at", precision: nil, null: false t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end @@ -56,8 +55,8 @@ create_table "assistantships", id: :serial, force: :cascade do |t| t.integer "user_id" t.integer "course_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["user_id", "course_id"], name: "index_assistantships_on_user_id_and_course_id", unique: true end @@ -73,7 +72,7 @@ t.integer "user_id", null: false t.integer "submission_id" t.string "name", null: false - t.datetime "created_at" + t.datetime "created_at", precision: nil t.boolean "awarded_after_soft_deadline", default: false, null: false t.index ["course_id", "user_id", "name"], name: "index_awarded_points_on_course_id_and_user_id_and_name", unique: true t.index ["course_id", "user_id", "submission_id"], name: "index_awarded_points_on_course_id_and_user_id_and_submission_id" @@ -83,8 +82,8 @@ create_table "banned_emails", force: :cascade do |t| t.string "email", null: false - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end create_table "certificates", id: :serial, force: :cascade do |t| @@ -92,8 +91,8 @@ t.binary "pdf" t.integer "user_id" t.integer "course_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["course_id"], name: "index_certificates_on_course_id" t.index ["user_id"], name: "index_certificates_on_user_id" end @@ -103,8 +102,8 @@ t.string "message" t.integer "sender_id" t.integer "course_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["course_id"], name: "index_course_notifications_on_course_id" end @@ -125,8 +124,8 @@ end create_table "course_template_refreshes", id: :serial, force: :cascade do |t| - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.integer "status", default: 0, null: false t.decimal "percent_done", precision: 10, scale: 4, default: "0.0", null: false t.jsonb "langs_refresh_output" @@ -147,21 +146,21 @@ t.integer "cached_version", default: 0, null: false t.string "source_backend", default: "git", null: false t.string "git_branch", default: "master", null: false - t.datetime "expires_at" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "expires_at", precision: nil + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil end create_table "courses", id: :serial, force: :cascade do |t| t.string "name" - t.datetime "created_at" - t.datetime "updated_at" - t.datetime "hide_after" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil + t.datetime "hide_after", precision: nil t.boolean "hidden", default: false, null: false t.integer "cached_version", default: 0, null: false t.string "spreadsheet_key" - t.datetime "hidden_if_registered_after" - t.datetime "refreshed_at" + t.datetime "hidden_if_registered_after", precision: nil + t.datetime "refreshed_at", precision: nil t.boolean "locked_exercise_points_visible", default: true, null: false t.text "description" t.integer "paste_visibility" @@ -191,15 +190,15 @@ create_table "exercises", id: :serial, force: :cascade do |t| t.string "name" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.integer "course_id" - t.datetime "publish_time" + t.datetime "publish_time", precision: nil t.string "gdocs_sheet" t.boolean "hidden", default: false, null: false t.boolean "returnable_forced" t.string "checksum", default: "", null: false - t.datetime "solution_visible_after" + t.datetime "solution_visible_after", precision: nil t.boolean "has_tests", default: false, null: false t.text "deadline_spec" t.text "unlock_spec" @@ -225,8 +224,8 @@ t.string "exercise_name", null: false t.integer "submission_id" t.text "answer", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["feedback_question_id", "course_id", "exercise_name"], name: "index_feedback_answers_question_course_exercise" t.index ["submission_id"], name: "index_feedback_answers_question" end @@ -235,8 +234,8 @@ t.integer "course_id", null: false t.text "question", null: false t.string "kind", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.integer "position", null: false t.text "title" t.index ["course_id"], name: "index_feedback_questions_on_course_id" @@ -244,8 +243,8 @@ create_table "kafka_batch_update_points", id: :serial, force: :cascade do |t| t.integer "course_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.integer "user_id" t.integer "exercise_id" t.string "task_type", default: "progress", null: false @@ -258,8 +257,8 @@ t.integer "to_course_id" t.integer "original_submission_id" t.integer "new_submission_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.index ["from_course_id", "to_course_id", "original_submission_id", "new_submission_id"], name: "unique_values", unique: true end @@ -267,8 +266,8 @@ t.integer "user_id", null: false t.integer "course_id", null: false t.string "exercise_name", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["course_id"], name: "index_model_solution_access_logs_on_course_id" t.index ["user_id"], name: "index_model_solution_access_logs_on_user_id" end @@ -277,8 +276,8 @@ t.integer "user_id" t.integer "course_id" t.string "exercise_name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", precision: nil, null: false + t.datetime "updated_at", precision: nil, null: false t.integer "cost", default: 1, null: false t.index ["course_id"], name: "index_model_solution_token_useds_on_course_id" t.index ["user_id"], name: "index_model_solution_token_useds_on_user_id" @@ -290,8 +289,8 @@ t.string "token", null: false t.integer "expires_in", null: false t.text "redirect_uri", null: false - t.datetime "created_at", null: false - t.datetime "revoked_at" + t.datetime "created_at", precision: nil, null: false + t.datetime "revoked_at", precision: nil t.string "scopes" t.index ["application_id"], name: "index_oauth_access_grants_on_application_id" t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true @@ -303,8 +302,8 @@ t.string "token", null: false t.string "refresh_token" t.integer "expires_in" - t.datetime "revoked_at" - t.datetime "created_at", null: false + t.datetime "revoked_at", precision: nil + t.datetime "created_at", precision: nil, null: false t.string "scopes" t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id" t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true @@ -318,8 +317,8 @@ t.string "secret", null: false t.text "redirect_uri", null: false t.string "scopes", default: "", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.boolean "confidential", default: true, null: false t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true end @@ -333,8 +332,8 @@ create_table "organization_memberships", force: :cascade do |t| t.bigint "user_id" t.bigint "organization_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["organization_id"], name: "index_organization_memberships_on_organization_id" t.index ["user_id", "organization_id"], name: "index_organization_memberships_on_user_id_and_organization_id", unique: true t.index ["user_id"], name: "index_organization_memberships_on_user_id" @@ -344,18 +343,18 @@ t.string "name" t.string "information" t.string "slug" - t.datetime "verified_at" + t.datetime "verified_at", precision: nil t.boolean "verified" t.boolean "disabled", default: false, null: false t.string "disabled_reason" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.boolean "hidden", default: false t.integer "creator_id" t.string "logo_file_name" t.string "logo_content_type" t.integer "logo_file_size" - t.datetime "logo_updated_at" + t.datetime "logo_updated_at", precision: nil t.string "phone" t.text "contact_information" t.string "email" @@ -367,16 +366,16 @@ create_table "points_upload_queues", id: :serial, force: :cascade do |t| t.integer "point_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil end create_table "recently_changed_user_details", id: :serial, force: :cascade do |t| t.integer "change_type", null: false t.string "old_value" t.string "new_value", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.string "username" t.string "email" t.integer "user_id" @@ -387,8 +386,8 @@ t.integer "feedback_answer_id" t.text "body" t.string "from" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["feedback_answer_id"], name: "index_reply_to_feedback_answers_on_feedback_answer_id" end @@ -396,8 +395,8 @@ t.integer "submission_id", null: false t.integer "reviewer_id" t.text "review_body", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.text "points" t.boolean "marked_as_read", default: false, null: false t.index ["reviewer_id"], name: "index_reviews_on_reviewer_id" @@ -407,8 +406,8 @@ create_table "sessions", id: :serial, force: :cascade do |t| t.string "session_id", null: false t.text "data" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["session_id"], name: "index_sessions_on_session_id" t.index ["updated_at"], name: "index_sessions_on_updated_at" end @@ -425,19 +424,19 @@ create_table "submissions", id: :serial, force: :cascade do |t| t.integer "user_id" t.text "pretest_error" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.string "exercise_name", null: false t.integer "course_id", null: false t.boolean "processed", default: false, null: false t.string "secret_token" t.boolean "all_tests_passed", default: false, null: false t.text "points" - t.datetime "processing_tried_at" - t.datetime "processing_began_at" - t.datetime "processing_completed_at" + t.datetime "processing_tried_at", precision: nil + t.datetime "processing_began_at", precision: nil + t.datetime "processing_completed_at", precision: nil t.integer "times_sent_to_sandbox", default: 0, null: false - t.datetime "processing_attempts_started_at" + t.datetime "processing_attempts_started_at", precision: nil t.integer "processing_priority", default: 0, null: false t.text "params_json" t.boolean "requires_review", default: false, null: false @@ -449,7 +448,7 @@ t.boolean "paste_available", default: false, null: false t.text "message_for_paste" t.string "paste_key" - t.datetime "client_time" + t.datetime "client_time", precision: nil t.bigint "client_nanotime" t.text "client_ip" t.string "sandbox" @@ -466,8 +465,8 @@ create_table "teacherships", id: :serial, force: :cascade do |t| t.integer "user_id" t.integer "organization_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["user_id", "organization_id"], name: "index_teacherships_on_user_id_and_organization_id", unique: true end @@ -476,8 +475,8 @@ t.text "test_case_name" t.text "message" t.boolean "successful" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.text "exception" t.text "detailed_message" t.index ["submission_id"], name: "index_test_case_runs_on_submission_id" @@ -488,15 +487,15 @@ t.string "exercise_name" t.string "files_hash" t.text "value" - t.datetime "created_at" + t.datetime "created_at", precision: nil t.index ["course_id", "exercise_name"], name: "index_test_scanner_cache_entries_on_course_id_and_exercise_name", unique: true end create_table "uncomputed_unlocks", id: :serial, force: :cascade do |t| t.integer "course_id", null: false t.integer "user_id", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["course_id", "user_id"], name: "index_uncomputed_unlocks_on_course_id_and_user_id" end @@ -504,8 +503,8 @@ t.integer "user_id", null: false t.integer "course_id", null: false t.string "exercise_name", null: false - t.datetime "valid_after" - t.datetime "created_at", null: false + t.datetime "valid_after", precision: nil + t.datetime "created_at", precision: nil, null: false t.index ["user_id", "course_id", "exercise_name"], name: "index_unlocks_on_user_id_and_course_id_and_exercise_name", unique: true end @@ -514,8 +513,8 @@ t.text "value" t.string "namespace" t.integer "user_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["user_id", "field_name", "namespace"], name: "index_user_app_data_on_user_id_and_field_name_and_namespace", unique: true end @@ -523,22 +522,23 @@ t.integer "user_id", null: false t.string "field_name", null: false t.text "value", null: false - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["user_id", "field_name"], name: "index_user_field_values_on_user_id_and_field_name", unique: true end create_table "users", id: :serial, force: :cascade do |t| t.string "login", null: false t.text "password_hash" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.string "salt" t.boolean "administrator", default: false, null: false t.text "email", default: "", null: false t.boolean "legitimate_student", default: true, null: false t.boolean "email_verified", default: false, null: false t.string "argon_hash" + t.boolean "password_managed_by_moocfi", default: false t.index "lower(email)", name: "index_user_email_lowercase", unique: true t.index ["login"], name: "index_users_on_login", unique: true end @@ -547,8 +547,8 @@ t.string "token", null: false t.integer "type", null: false t.integer "user_id" - t.datetime "created_at" - t.datetime "updated_at" + t.datetime "created_at", precision: nil + t.datetime "updated_at", precision: nil t.index ["user_id"], name: "index_verification_tokens_on_user_id" end From 28d495ecc015ee1a3b41ddd35672143e4b90d14a Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Fri, 11 Jul 2025 01:40:17 +0300 Subject: [PATCH 02/21] Validate request payload --- app/controllers/api/v8/users_controller.rb | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index 4b28c9eba..1fa753ffe 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -175,15 +175,20 @@ def set_password_managed_by_moocfi @user = User.find_by!(id: params[:id]) authorize! :update, @user - @user.password_managed_by_moocfi = params[:set_password_managed_by_moocfi] - if @user.save - render json: { - status: "Password managed by Mooc.fi set to #{params[:set_password_managed_by_moocfi]}." - } + + value = params[:set_password_managed_by_moocfi] + unless value.in?([true, false]) + @user.errors.add(:password_managed_by_moocfi, 'must be a boolean') + else + @user.password_managed_by_moocfi = value + end + + if @user.errors.any? || !@user.save + render json: { errors: @user.errors }, status: :bad_request else render json: { - errors: @user.errors - }, status: :bad_request + status: "Password managed by Mooc.fi set to #{value}." + } end end From 7a0ee923539d23b7c25cf862a1b86bc29ba9f8c1 Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Fri, 11 Jul 2025 01:49:15 +0300 Subject: [PATCH 03/21] Lessen code complexity for codeclimate --- app/controllers/api/v8/users_controller.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index 1fa753ffe..8702d2619 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -177,7 +177,7 @@ def set_password_managed_by_moocfi authorize! :update, @user value = params[:set_password_managed_by_moocfi] - unless value.in?([true, false]) + if !boolean_param?(value) @user.errors.add(:password_managed_by_moocfi, 'must be a boolean') else @user.password_managed_by_moocfi = value @@ -193,6 +193,10 @@ def set_password_managed_by_moocfi end private + def boolean_param?(value) + value.is_a?(TrueClass) || value.is_a?(FalseClass) + end + def set_email user_params = params[:user] From 17105ed266fce2d035a1da0f2c1a18799d0863f0 Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Fri, 11 Jul 2025 01:55:00 +0300 Subject: [PATCH 04/21] Further lessen code complexity --- app/controllers/api/v8/users_controller.rb | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index 8702d2619..f01b46392 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -172,24 +172,21 @@ def update def set_password_managed_by_moocfi unauthorize_guest! if current_user.guest? - @user = User.find_by!(id: params[:id]) authorize! :update, @user value = params[:set_password_managed_by_moocfi] - if !boolean_param?(value) + unless boolean_param?(value) @user.errors.add(:password_managed_by_moocfi, 'must be a boolean') - else - @user.password_managed_by_moocfi = value + return render json: { errors: @user.errors }, status: :bad_request end - if @user.errors.any? || !@user.save - render json: { errors: @user.errors }, status: :bad_request - else - render json: { - status: "Password managed by Mooc.fi set to #{value}." - } - end + @user.password_managed_by_moocfi = value + return render json: { errors: @user.errors }, status: :bad_request unless @user.save + + render json: { + status: "Password managed by Mooc.fi set to #{value}." + } end private From e8e92965bb01a43ebb027e8530cbc0b29719fa63 Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Fri, 11 Jul 2025 02:35:50 +0300 Subject: [PATCH 05/21] Fix model spec --- .../20250710194038_add_password_managed_by_moocfi_to_users.rb | 2 +- db/schema.rb | 2 +- spec/factories.rb | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/db/migrate/20250710194038_add_password_managed_by_moocfi_to_users.rb b/db/migrate/20250710194038_add_password_managed_by_moocfi_to_users.rb index 1e88db464..afc4df9f3 100644 --- a/db/migrate/20250710194038_add_password_managed_by_moocfi_to_users.rb +++ b/db/migrate/20250710194038_add_password_managed_by_moocfi_to_users.rb @@ -1,5 +1,5 @@ class AddPasswordManagedByMoocfiToUsers < ActiveRecord::Migration[7.1] def change - add_column :users, :password_managed_by_moocfi, :boolean, default: false + add_column :users, :password_managed_by_moocfi, :boolean, default: false, null: false end end diff --git a/db/schema.rb b/db/schema.rb index e27f4f729..4b336cd24 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -538,7 +538,7 @@ t.boolean "legitimate_student", default: true, null: false t.boolean "email_verified", default: false, null: false t.string "argon_hash" - t.boolean "password_managed_by_moocfi", default: false + t.boolean "password_managed_by_moocfi", default: false, null: false t.index "lower(email)", name: "index_user_email_lowercase", unique: true t.index ["login"], name: "index_users_on_login", unique: true end diff --git a/spec/factories.rb b/spec/factories.rb index 3d0872957..fa7bbc1a3 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -31,6 +31,7 @@ def make_repo_for_course_template sequence(:email) { |n| "user#{n}@example.com" } sequence(:email_verified) { |n| 'false' } administrator { false } + password_managed_by_moocfi { false } end factory :verified_user, class: User do @@ -39,6 +40,7 @@ def make_repo_for_course_template sequence(:email) { |n| "ver_user#{n}@example.com" } sequence(:email_verified) { |n| 'true' } administrator { false } + password_managed_by_moocfi { false } end factory :admin, class: User do @@ -47,6 +49,7 @@ def make_repo_for_course_template sequence(:email) { |n| "admin#{n}@example.com" } sequence(:email_verified) { |n| 'true' } administrator { true } + password_managed_by_moocfi { false } end factory :course, class: Course do From c700924746944a4e09ba59f2d565c0fee6ad0bac Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Fri, 11 Jul 2025 03:22:35 +0300 Subject: [PATCH 06/21] Prune docker before spec_models --- .github/workflows/ci.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8777124c0..423b3fd29 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,12 +29,16 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - with: + with: submodules: false - name: Setup git submodules run: | sed -i 's/git@github.com:/https:\/\/github.com\//' .gitmodules git submodule update --init --recursive + - name: Prune Docker system (remove old containers, images, and volumes) + run: | + docker compose down -v || true + docker system prune -af || true - name: Docker compose build run: docker compose build - name: Setup database & test env @@ -47,7 +51,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - with: + with: submodules: false - name: Setup git submodules run: | @@ -65,7 +69,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - with: + with: submodules: false - name: Setup git submodules run: | @@ -83,7 +87,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - with: + with: submodules: false - name: Setup git submodules run: | @@ -101,7 +105,7 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v2 - with: + with: submodules: false - name: Setup git submodules run: | @@ -112,4 +116,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 From a016ffd330a8a6dc9ea2b70141798ec796500b62 Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Fri, 11 Jul 2025 22:02:41 +0300 Subject: [PATCH 07/21] Revert docker pruning before spec_models --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 423b3fd29..2f359a147 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,6 @@ jobs: run: | sed -i 's/git@github.com:/https:\/\/github.com\//' .gitmodules git submodule update --init --recursive - - name: Prune Docker system (remove old containers, images, and volumes) - run: | - docker compose down -v || true - docker system prune -af || true - name: Docker compose build run: docker compose build - name: Setup database & test env From 4940b770ca2f8b61ae0cf1ae2f4c4e54ae4a0bd4 Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Sat, 12 Jul 2025 03:01:08 +0300 Subject: [PATCH 08/21] Specify courses.mooc.fi, delete password details from db --- app/controllers/api/v8/users_controller.rb | 44 +++++++++---------- config/routes.rb | 2 +- ...add_password_managed_by_moocfi_to_users.rb | 5 --- db/schema.rb | 4 +- 4 files changed, 24 insertions(+), 31 deletions(-) delete mode 100644 db/migrate/20250710194038_add_password_managed_by_moocfi_to_users.rb diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index f01b46392..2ecd71288 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -56,26 +56,27 @@ class UsersController < Api::V8::BaseController end end - swagger_path '/api/v8/users/{user_id}/set_password_managed_by_moocfi' do + 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_moocfi for the user with the given id to payload value.' - key :operationId, 'setPasswordManagedByMoocfi' + 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_moocfi" + 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 Mooc.fi set to true.' + property :status, type: :string, example: 'Password managed by courses.mooc.fi set to true.' end end end end + skip_authorization_check only: %i[set_password_managed_by_courses_mooc_fi] def show unauthorize_guest! if current_user.guest? @@ -170,30 +171,27 @@ def update }, status: :bad_request end - def set_password_managed_by_moocfi - unauthorize_guest! if current_user.guest? - @user = User.find_by!(id: params[:id]) - authorize! :update, @user + def set_password_managed_by_courses_mooc_fi + only_admins! - value = params[:set_password_managed_by_moocfi] - unless boolean_param?(value) - @user.errors.add(:password_managed_by_moocfi, 'must be a boolean') - return render json: { errors: @user.errors }, status: :bad_request + 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.save! + 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 - - @user.password_managed_by_moocfi = value - return render json: { errors: @user.errors }, status: :bad_request unless @user.save - render json: { - status: "Password managed by Mooc.fi set to #{value}." - } + errors: @user.errors + }, status: :bad_request end private - def boolean_param?(value) - value.is_a?(TrueClass) || value.is_a?(FalseClass) - end - def set_email user_params = params[:user] diff --git a/config/routes.rb b/config/routes.rb index 6daa54f56..04fc52c93 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -59,7 +59,7 @@ resources :request_deletion, only: [:create], module: :users resources :assistantships, module: :users, only: :index resources :teacherships, module: :users, only: :index - post :set_password_managed_by_moocfi, on: :member + post :set_password_managed_by_courses_mooc_fi, on: :member end resources :user_app_datum, only: [:index] diff --git a/db/migrate/20250710194038_add_password_managed_by_moocfi_to_users.rb b/db/migrate/20250710194038_add_password_managed_by_moocfi_to_users.rb deleted file mode 100644 index afc4df9f3..000000000 --- a/db/migrate/20250710194038_add_password_managed_by_moocfi_to_users.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddPasswordManagedByMoocfiToUsers < ActiveRecord::Migration[7.1] - def change - add_column :users, :password_managed_by_moocfi, :boolean, default: false, null: false - end -end diff --git a/db/schema.rb b/db/schema.rb index 4b336cd24..25cc8c9f3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_07_10_194038) do +ActiveRecord::Schema[7.1].define(version: 2025_07_11_214850) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -538,7 +538,7 @@ t.boolean "legitimate_student", default: true, null: false t.boolean "email_verified", default: false, null: false t.string "argon_hash" - t.boolean "password_managed_by_moocfi", default: false, null: false + t.boolean "password_managed_by_courses_mooc_fi", default: false, null: false t.index "lower(email)", name: "index_user_email_lowercase", unique: true t.index ["login"], name: "index_users_on_login", unique: true end From 7f192b6f191d73686b0c47b217d1d93e188b8f6d Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Sat, 12 Jul 2025 03:02:40 +0300 Subject: [PATCH 09/21] Add migration file, fix rubocop error --- app/controllers/api/v8/users_controller.rb | 2 +- ...14850_add_password_managed_by_courses_mooc_fi_to_users.rb | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20250711214850_add_password_managed_by_courses_mooc_fi_to_users.rb diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index 2ecd71288..590d0f10c 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -183,7 +183,7 @@ def set_password_managed_by_courses_mooc_fi @user.save! 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." + status: 'Password managed by courses.mooc.fi set to true and password deleted.' } end render json: { diff --git a/db/migrate/20250711214850_add_password_managed_by_courses_mooc_fi_to_users.rb b/db/migrate/20250711214850_add_password_managed_by_courses_mooc_fi_to_users.rb new file mode 100644 index 000000000..0f70f37af --- /dev/null +++ b/db/migrate/20250711214850_add_password_managed_by_courses_mooc_fi_to_users.rb @@ -0,0 +1,5 @@ +class AddPasswordManagedByCoursesMoocFiToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :password_managed_by_courses_mooc_fi, :boolean, default: false, null: false + end +end From d71bdd8de91804234c29e917aab63153b5b3d476 Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Sat, 12 Jul 2025 03:07:29 +0300 Subject: [PATCH 10/21] Update factorybot user for specs --- spec/factories.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/factories.rb b/spec/factories.rb index fa7bbc1a3..8f0c4c416 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -31,7 +31,7 @@ def make_repo_for_course_template sequence(:email) { |n| "user#{n}@example.com" } sequence(:email_verified) { |n| 'false' } administrator { false } - password_managed_by_moocfi { false } + password_managed_by_courses_mooc_fi { false } end factory :verified_user, class: User do @@ -40,7 +40,7 @@ def make_repo_for_course_template sequence(:email) { |n| "ver_user#{n}@example.com" } sequence(:email_verified) { |n| 'true' } administrator { false } - password_managed_by_moocfi { false } + password_managed_by_courses_mooc_fi { false } end factory :admin, class: User do @@ -49,7 +49,7 @@ def make_repo_for_course_template sequence(:email) { |n| "admin#{n}@example.com" } sequence(:email_verified) { |n| 'true' } administrator { true } - password_managed_by_moocfi { false } + password_managed_by_courses_mooc_fi { false } end factory :course, class: Course do From 968b8758b89f34954a49a1ca5b17b591ab7de9b7 Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Sun, 13 Jul 2025 16:30:23 +0300 Subject: [PATCH 11/21] Don't call @user.save twice --- app/controllers/api/v8/users_controller.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index 590d0f10c..294d676ff 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -70,7 +70,7 @@ class UsersController < Api::V8::BaseController schema do key :title, :status key :required, [:status] - property :status, type: :string, example: 'Password managed by courses.mooc.fi set to true.' + property :status, type: :string, example: 'Password managed by courses.mooc.fi set to true and password deleted.' end end end @@ -180,7 +180,6 @@ def set_password_managed_by_courses_mooc_fi @user.password_hash = nil @user.salt = nil @user.argon_hash = nil - @user.save! 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.' From ce1f10a61c56a1ce9246b1c1f3196d6385898c48 Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Tue, 15 Jul 2025 10:18:05 +0300 Subject: [PATCH 12/21] Authenticate via courses.mooc.fi if boolean true --- app/models/user.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index 721e5c041..d644fd06c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -146,9 +146,27 @@ def self.authenticate(login, submitted_password) user = find_by(login: login) user ||= find_by('lower(email) = ?', login.downcase) return nil if user.nil? + user if user.password_managed_by_courses_mooc_fi && authenticate_via_courses_mooc_fi(user.email, submitted_password) user if user.has_password?(submitted_password) end + def authenticate_via_courses_mooc_fi(email, submitted_password) + uri = URI.parse('https://courses.mooc.fi/api/v0/tmc-server/auth') + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = (uri.scheme == 'https') + request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json' }) + request.body = { email: email, password: submitted_password }.to_json + + response = http.request(request) + return false unless response.is_a?(Net::HTTPSuccess) + + data = JSON.parse(response.body) + data['authenticated'] == true + rescue StandardError => e + Rails.logger.error("MOOC.fi authentication failed: #{e}") + false + end + def password_reset_key action_tokens.find { |t| t.action == 'reset_password' } end From e0cdbae42b45aa0fd668759dbbd62ff932f87e21 Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Tue, 22 Jul 2025 23:23:04 +0300 Subject: [PATCH 13/21] Use rest-client for authentication request --- app/controllers/api/v8/users_controller.rb | 12 ++++---- app/models/user.rb | 33 ++++++++++++++-------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index 294d676ff..842153496 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -175,12 +175,12 @@ 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 - raise ActiveRecord::Rollback if !@user.errors.empty? || !@user.save + 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 + 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.' } diff --git a/app/models/user.rb b/app/models/user.rb index d644fd06c..7700a608a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'rest-client' + class User < ApplicationRecord include Comparable include Gravtastic @@ -151,20 +153,27 @@ def self.authenticate(login, submitted_password) end def authenticate_via_courses_mooc_fi(email, submitted_password) - uri = URI.parse('https://courses.mooc.fi/api/v0/tmc-server/auth') - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = (uri.scheme == 'https') - request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json' }) - request.body = { email: email, password: submitted_password }.to_json - - response = http.request(request) - return false unless response.is_a?(Net::HTTPSuccess) + auth_url = SiteSetting.value('courses_mooc_fi_auth_url') + response = RestClient.post( + auth_url, + { email: email, password: submitted_password }.to_json, + { content_type: :json, accept: :json } + ) data = JSON.parse(response.body) - data['authenticated'] == true - rescue StandardError => e - Rails.logger.error("MOOC.fi authentication failed: #{e}") - false + unless data["authenticated"] == true + raise "Authentication via courses.mooc.fi failed for #{email}" + end + + true + rescue RestClient::Unauthorized, RestClient::Forbidden + raise "Authentication rejected by courses.mooc.fi for #{email}" + rescue RestClient::ExceptionWithResponse => e + Rails.logger.error("Authentication via courses.mooc.fi error: #{e.response}") + raise "Authentication via courses.mooc.fi failed: #{e.message}" + rescue => e + Rails.logger.error("Unexpected error during authentication via courses.mooc.fi: #{e.message}") + raise "Unexpected error while authenticating via courses.mooc.fi: #{e.message}" end def password_reset_key From a900bc51b026b782a46ec5987e2c22130191f30d Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Wed, 23 Jul 2025 02:32:49 +0300 Subject: [PATCH 14/21] Update password through courses if boolean is set --- app/controllers/api/v8/users_controller.rb | 4 +++ app/controllers/settings_controller.rb | 4 +++ app/controllers/users_controller.rb | 4 +++ app/models/user.rb | 30 +++++++++++++++++++++- 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index 842153496..9831b1b07 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -230,6 +230,10 @@ def set_password end def maybe_update_password + if @user.password_managed_by_courses_mooc_fi + return @user.update_password_via_courses_mooc_fi(@user.email, 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') diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index 1915ac4c9..c6b7c8a08 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 + return user.update_password_via_courses_mooc_fi(user.email, 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..a345da6e9 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 + return user.update_password_via_courses_mooc_fi(user.email, 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/models/user.rb b/app/models/user.rb index 7700a608a..9f12977c9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -161,7 +161,7 @@ def authenticate_via_courses_mooc_fi(email, submitted_password) ) data = JSON.parse(response.body) - unless data["authenticated"] == true + unless data['authenticated'] == true raise "Authentication via courses.mooc.fi failed for #{email}" end @@ -176,6 +176,34 @@ def authenticate_via_courses_mooc_fi(email, submitted_password) raise "Unexpected error while authenticating via courses.mooc.fi: #{e.message}" end + def update_password_via_courses_mooc_fi(email, old_password, new_password) + update_url = SiteSetting.value('courses_mooc_fi_update_password_url') + + response = RestClient.put( + update_url, + { + email: email, + old_password: old_password, + new_password: new_password + }.to_json, + { content_type: :json, accept: :json } + ) + + data = JSON.parse(response.body) + + unless data['updated'] == true + raise "Updating password via courses.mooc.fi failed for #{email}" + end + + true + rescue RestClient::ExceptionWithResponse => e + Rails.logger.error("Updating password via courses.mooc.fi failed for #{email}: #{e.response}") + false + rescue => e + Rails.logger.error("Unexpected error updating password via courses.mooc.fi for #{email}: #{e.message}") + false + end + def password_reset_key action_tokens.find { |t| t.action == 'reset_password' } end From 0286f75ed7d30a8d637f44f256d789779959014d Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Wed, 23 Jul 2025 15:23:10 +0300 Subject: [PATCH 15/21] Add courses.mooc.fi user id to users-table --- .../20250723122117_add_courses_mooc_fi_user_id_to_users.rb | 6 ++++++ db/schema.rb | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20250723122117_add_courses_mooc_fi_user_id_to_users.rb diff --git a/db/migrate/20250723122117_add_courses_mooc_fi_user_id_to_users.rb b/db/migrate/20250723122117_add_courses_mooc_fi_user_id_to_users.rb new file mode 100644 index 000000000..8c478a373 --- /dev/null +++ b/db/migrate/20250723122117_add_courses_mooc_fi_user_id_to_users.rb @@ -0,0 +1,6 @@ +class AddCoursesMoocFiUserIdToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :courses_mooc_fi_user_id, :string + add_index :users, :courses_mooc_fi_user_id, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 25cc8c9f3..2bef330e0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2025_07_11_214850) do +ActiveRecord::Schema[7.1].define(version: 2025_07_23_122117) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -539,7 +539,9 @@ t.boolean "email_verified", default: false, null: false t.string "argon_hash" t.boolean "password_managed_by_courses_mooc_fi", default: false, null: false + t.string "courses_mooc_fi_user_id" t.index "lower(email)", name: "index_user_email_lowercase", unique: true + t.index ["courses_mooc_fi_user_id"], name: "index_users_on_courses_mooc_fi_user_id", unique: true t.index ["login"], name: "index_users_on_login", unique: true end From cc333a4b4773c3405bb23ecb9a02f94ca3fd0bed Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Wed, 23 Jul 2025 15:39:39 +0300 Subject: [PATCH 16/21] User courses.mooc.fi user id instead of email --- app/controllers/api/v8/users_controller.rb | 4 ++-- app/controllers/settings_controller.rb | 4 ++-- app/controllers/users_controller.rb | 4 ++-- app/models/user.rb | 16 ++++++++-------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index 9831b1b07..3f62f637e 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -230,8 +230,8 @@ def set_password end def maybe_update_password - if @user.password_managed_by_courses_mooc_fi - return @user.update_password_via_courses_mooc_fi(@user.email, user_params[:old_password], user_params[: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? diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index c6b7c8a08..21e8f2801 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -86,8 +86,8 @@ def set_email end def maybe_update_password(user, user_params) - if user.password_managed_by_courses_mooc_fi - return user.update_password_via_courses_mooc_fi(user.email, user_params[:old_password], user_params[: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 user_params[:old_password].present? || user_params[:password].present? diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a345da6e9..be012b747 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -179,8 +179,8 @@ def set_password end def maybe_update_password(user, user_params) - if user.password_managed_by_courses_mooc_fi - return user.update_password_via_courses_mooc_fi(user.email, user_params[:old_password], user_params[: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 user_params[:old_password].present? || user_params[:password].present? diff --git a/app/models/user.rb b/app/models/user.rb index 9f12977c9..cc2750107 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -148,15 +148,15 @@ def self.authenticate(login, submitted_password) user = find_by(login: login) user ||= find_by('lower(email) = ?', login.downcase) return nil if user.nil? - user if user.password_managed_by_courses_mooc_fi && authenticate_via_courses_mooc_fi(user.email, submitted_password) + user if user.password_managed_by_courses_mooc_fi && user.courses_mooc_fi_user_id.present? && authenticate_via_courses_mooc_fi(user.courses_mooc_fi_user_id, submitted_password) user if user.has_password?(submitted_password) end - def authenticate_via_courses_mooc_fi(email, submitted_password) + def authenticate_via_courses_mooc_fi(courses_mooc_fi_user_id, submitted_password) auth_url = SiteSetting.value('courses_mooc_fi_auth_url') response = RestClient.post( auth_url, - { email: email, password: submitted_password }.to_json, + { user_id: courses_mooc_fi_user_id, password: submitted_password }.to_json, { content_type: :json, accept: :json } ) @@ -176,13 +176,13 @@ def authenticate_via_courses_mooc_fi(email, submitted_password) raise "Unexpected error while authenticating via courses.mooc.fi: #{e.message}" end - def update_password_via_courses_mooc_fi(email, old_password, new_password) + def update_password_via_courses_mooc_fi(courses_mooc_fi_user_id, old_password, new_password) update_url = SiteSetting.value('courses_mooc_fi_update_password_url') response = RestClient.put( update_url, { - email: email, + user_id: courses_mooc_fi_user_id, old_password: old_password, new_password: new_password }.to_json, @@ -192,15 +192,15 @@ def update_password_via_courses_mooc_fi(email, old_password, new_password) data = JSON.parse(response.body) unless data['updated'] == true - raise "Updating password via courses.mooc.fi failed for #{email}" + raise "Updating password via courses.mooc.fi failed for user with courses.mooc.fi-user-id #{courses_mooc_fi_user_id}" end true rescue RestClient::ExceptionWithResponse => e - Rails.logger.error("Updating password via courses.mooc.fi failed for #{email}: #{e.response}") + Rails.logger.error("Updating password via courses.mooc.fi failed for user with courses.mooc.fi-user-id #{courses_mooc_fi_user_id}: #{e.response}") false rescue => e - Rails.logger.error("Unexpected error updating password via courses.mooc.fi for #{email}: #{e.message}") + Rails.logger.error("Unexpected error updating password via courses.mooc.fi for user with courses.mooc.fi-user-id #{courses_mooc_fi_user_id}: #{e.message}") false end From 0794e9402eec9833e1d94e135a3894722941bf2c Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Wed, 23 Jul 2025 18:34:55 +0300 Subject: [PATCH 17/21] Add courses.mooc.fi user id when moving password management --- app/controllers/api/v8/users_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index 3f62f637e..d0fc74467 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -180,6 +180,7 @@ def set_password_managed_by_courses_mooc_fi 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.' From 9f486e54a55fe883fd0abbcadde8d4230e283a3e Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Mon, 28 Jul 2025 22:04:02 +0300 Subject: [PATCH 18/21] Authorize requests to courses.mooc.fi --- app/models/user.rb | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index cc2750107..46e45802d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -156,8 +156,14 @@ def authenticate_via_courses_mooc_fi(courses_mooc_fi_user_id, submitted_password auth_url = SiteSetting.value('courses_mooc_fi_auth_url') response = RestClient.post( auth_url, - { user_id: courses_mooc_fi_user_id, password: submitted_password }.to_json, - { content_type: :json, accept: :json } + { + user_id: courses_mooc_fi_user_id, + password: submitted_password }.to_json, + { + content_type: :json, + accept: :json, + Authorization: Rails.application.secrets.tmc_server_secret_for_communicating_to_secret_project, + } ) data = JSON.parse(response.body) @@ -184,9 +190,13 @@ def update_password_via_courses_mooc_fi(courses_mooc_fi_user_id, old_password, n { user_id: courses_mooc_fi_user_id, old_password: old_password, - new_password: new_password + new_password: new_password, }.to_json, - { content_type: :json, accept: :json } + { + content_type: :json, + accept: :json, + Authorization: Rails.application.secrets.tmc_server_secret_for_communicating_to_secret_project, + } ) data = JSON.parse(response.body) From a38d633477867f9b7f72a8c0dd04f4f85a9726fa Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Tue, 26 Aug 2025 14:13:24 +0300 Subject: [PATCH 19/21] Add enpoint for getting user details by email --- app/controllers/api/v8/apidocs_controller.rb | 7 +++ app/controllers/api/v8/users_controller.rb | 65 +++++++++++++++++--- app/models/user.rb | 3 +- config/routes.rb | 1 + 4 files changed, 67 insertions(+), 9 deletions(-) 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 d0fc74467..194fe4953 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -76,6 +76,32 @@ class UsersController < Api::V8::BaseController 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 @@ -125,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) @@ -191,6 +211,24 @@ def set_password_managed_by_courses_mooc_fi }, status: :bad_request end + def get_user_with_email + unauthorize_guest! if current_user.guest? + + user = User.find_by!(email: params[:email]) + logger.info user + 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] @@ -275,6 +313,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/models/user.rb b/app/models/user.rb index 46e45802d..1f138d07d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -158,7 +158,8 @@ def authenticate_via_courses_mooc_fi(courses_mooc_fi_user_id, submitted_password auth_url, { user_id: courses_mooc_fi_user_id, - password: submitted_password }.to_json, + password: submitted_password, + }.to_json, { content_type: :json, accept: :json, diff --git a/config/routes.rb b/config/routes.rb index 04fc52c93..ec23dd824 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -60,6 +60,7 @@ resources :assistantships, module: :users, only: :index resources :teacherships, module: :users, only: :index post :set_password_managed_by_courses_mooc_fi, on: :member + get :get_user_with_email, on: :collection end resources :user_app_datum, only: [:index] From 3e86cc41fae20bbea326b8c66aab7eb67b23aae4 Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Tue, 26 Aug 2025 14:27:57 +0300 Subject: [PATCH 20/21] Remove unnecessary logging --- app/controllers/api/v8/users_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index 194fe4953..109af9380 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -215,7 +215,6 @@ def get_user_with_email unauthorize_guest! if current_user.guest? user = User.find_by!(email: params[:email]) - logger.info user authorize! :read, user name = UserFieldValue.where(user_id: user.id, field_name: ['first_name', 'last_name']).pluck(:field_name, :value).to_h From fcf8befc2d70097d05a144ee36a9c77cdbbe29d5 Mon Sep 17 00:00:00 2001 From: Antti Leinonen Date: Tue, 23 Sep 2025 12:12:13 +0300 Subject: [PATCH 21/21] Add api v8 endpoint for deleting users --- app/controllers/api/v8/users_controller.rb | 22 ++++++++++++++++++++++ config/routes.rb | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v8/users_controller.rb b/app/controllers/api/v8/users_controller.rb index 109af9380..a2708f4c0 100644 --- a/app/controllers/api/v8/users_controller.rb +++ b/app/controllers/api/v8/users_controller.rb @@ -191,6 +191,28 @@ def update }, status: :bad_request end + def destroy + unauthorize_guest! if current_user.guest? + + user = User.find(params[:id]) + authorize! :destroy, user + + 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 + def set_password_managed_by_courses_mooc_fi only_admins! diff --git a/config/routes.rb b/config/routes.rb index ec23dd824..720ce34be 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,7 +55,7 @@ resources :recently_changed_user_details, only: :index end - resources :users, only: %i[show create update] do + resources :users, only: %i[show create update destroy] do resources :request_deletion, only: [:create], module: :users resources :assistantships, module: :users, only: :index resources :teacherships, module: :users, only: :index