From 0e7844254c2c23486ed87a7f7303f6ecc8977308 Mon Sep 17 00:00:00 2001 From: Waelalahamdi Date: Sun, 10 Aug 2025 10:00:53 +1000 Subject: [PATCH] feat: add observer role access control with migration and tests --- app/helpers/authorisation_helpers.rb | 4 ++ ...250803223033_add_observer_to_unit_roles.rb | 5 ++ db/schema.rb | 71 ++++++++++--------- test/helpers/authorisation_helpers_test.rb | 61 ++++++++++++++++ 4 files changed, 106 insertions(+), 35 deletions(-) create mode 100644 db/migrate/20250803223033_add_observer_to_unit_roles.rb create mode 100644 test/helpers/authorisation_helpers_test.rb diff --git a/app/helpers/authorisation_helpers.rb b/app/helpers/authorisation_helpers.rb index d09cbf75a..30ce1d5e2 100644 --- a/app/helpers/authorisation_helpers.rb +++ b/app/helpers/authorisation_helpers.rb @@ -16,6 +16,10 @@ def authorise?(user, object, action, perm_get_fn = method(:get_permission_hash), obj_class = object.class == Class ? object : object.class role_obj = object.role_for(user) + # To user read only + if role_obj.respond_to?(:observer) && role_obj.observer && action != :get + return false + end return false if role_obj.nil? diff --git a/db/migrate/20250803223033_add_observer_to_unit_roles.rb b/db/migrate/20250803223033_add_observer_to_unit_roles.rb new file mode 100644 index 000000000..d8eb947e1 --- /dev/null +++ b/db/migrate/20250803223033_add_observer_to_unit_roles.rb @@ -0,0 +1,5 @@ +class AddObserverToUnitRoles < ActiveRecord::Migration[7.1] + def change + add_column :unit_roles, :observer, :boolean, default: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 6daa71ebf..46fcbcf7c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,8 +10,8 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_05_28_223908) do - create_table "activity_types", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| +ActiveRecord::Schema[7.1].define(version: 2025_08_03_223033) do + create_table "activity_types", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false t.datetime "created_at", null: false @@ -20,21 +20,21 @@ t.index ["name"], name: "index_activity_types_on_name", unique: true end - create_table "auth_tokens", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "auth_tokens", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.datetime "auth_token_expiry", null: false t.bigint "user_id" t.string "authentication_token", null: false t.index ["user_id"], name: "index_auth_tokens_on_user_id" end - create_table "breaks", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "breaks", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.datetime "start_date", null: false t.integer "number_of_weeks", null: false t.bigint "teaching_period_id" t.index ["teaching_period_id"], name: "index_breaks_on_teaching_period_id" end - create_table "campuses", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "campuses", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name", null: false t.integer "mode", null: false t.string "abbreviation", null: false @@ -44,7 +44,7 @@ t.index ["name"], name: "index_campuses_on_name", unique: true end - create_table "comments_read_receipts", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "comments_read_receipts", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "task_comment_id", null: false t.bigint "user_id", null: false t.datetime "created_at", null: false @@ -54,7 +54,7 @@ t.index ["user_id"], name: "index_comments_read_receipts_on_user_id" end - create_table "discussion_comments", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "discussion_comments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.datetime "time_started" t.datetime "time_completed" t.integer "number_of_prompts" @@ -62,7 +62,7 @@ t.datetime "updated_at", null: false end - create_table "group_memberships", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "group_memberships", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "group_id" t.bigint "project_id" t.boolean "active", default: true @@ -72,7 +72,7 @@ t.index ["project_id"], name: "index_group_memberships_on_project_id" end - create_table "group_sets", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "group_sets", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.string "name" t.boolean "allow_students_to_create_groups", default: true @@ -85,7 +85,7 @@ t.index ["unit_id"], name: "index_group_sets_on_unit_id" end - create_table "group_submissions", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "group_submissions", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "group_id" t.string "notes" t.bigint "submitted_by_project_id" @@ -97,7 +97,7 @@ t.index ["task_definition_id"], name: "index_group_submissions_on_task_definition_id" end - create_table "groups", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "groups", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "group_set_id" t.bigint "tutorial_id" t.string "name" @@ -109,7 +109,7 @@ t.index ["tutorial_id"], name: "index_groups_on_tutorial_id" end - create_table "learning_outcome_task_links", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "learning_outcome_task_links", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.text "description" t.integer "rating" t.bigint "task_definition_id" @@ -122,7 +122,7 @@ t.index ["task_id"], name: "index_learning_outcome_task_links_on_task_id" end - create_table "learning_outcomes", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "learning_outcomes", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.integer "ilo_number" t.string "name" @@ -131,7 +131,7 @@ t.index ["unit_id"], name: "index_learning_outcomes_on_unit_id" end - create_table "logins", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "logins", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.datetime "timestamp" t.bigint "user_id" t.datetime "created_at", null: false @@ -139,7 +139,7 @@ t.index ["user_id"], name: "index_logins_on_user_id" end - create_table "overseer_assessments", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "overseer_assessments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "task_id", null: false t.string "submission_timestamp", null: false t.string "result_task_status" @@ -150,7 +150,7 @@ t.index ["task_id"], name: "index_overseer_assessments_on_task_id" end - create_table "overseer_images", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "overseer_images", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name", null: false t.string "tag", null: false t.datetime "created_at", null: false @@ -160,7 +160,7 @@ t.datetime "last_pulled_date" end - create_table "projects", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "projects", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.string "project_role" t.datetime "created_at", null: false @@ -187,14 +187,14 @@ t.index ["user_id"], name: "index_projects_on_user_id" end - create_table "roles", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "roles", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name" t.text "description" t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "task_comments", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "task_comments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "task_id", null: false t.bigint "user_id", null: false t.string "comment", limit: 4096 @@ -225,7 +225,7 @@ t.index ["user_id"], name: "index_task_comments_on_user_id" end - create_table "task_definitions", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "task_definitions", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.string "name" t.string "description", limit: 4096 @@ -256,7 +256,7 @@ t.index ["unit_id"], name: "index_task_definitions_on_unit_id" end - create_table "task_engagements", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "task_engagements", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.datetime "engagement_time" t.string "engagement" t.bigint "task_id" @@ -265,7 +265,7 @@ t.index ["task_id"], name: "index_task_engagements_on_task_id" end - create_table "task_pins", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "task_pins", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "task_id", null: false t.bigint "user_id", null: false t.datetime "created_at", null: false @@ -275,7 +275,7 @@ t.index ["user_id"], name: "fk_rails_915df186ed" end - create_table "task_similarities", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "task_similarities", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "task_id" t.bigint "other_task_id" t.integer "pct" @@ -290,14 +290,14 @@ t.index ["tii_submission_id"], name: "index_task_similarities_on_tii_submission_id" end - create_table "task_statuses", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "task_statuses", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name" t.string "description" t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "task_submissions", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "task_submissions", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.datetime "submission_time" t.datetime "assessment_time" t.string "outcome" @@ -309,7 +309,7 @@ t.index ["task_id"], name: "index_task_submissions_on_task_id" end - create_table "tasks", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "tasks", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "task_definition_id" t.bigint "project_id" t.bigint "task_status_id" @@ -335,7 +335,7 @@ t.index ["task_status_id"], name: "index_tasks_on_task_status_id" end - create_table "teaching_periods", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "teaching_periods", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "period", null: false t.datetime "start_date", null: false t.datetime "end_date", null: false @@ -394,7 +394,7 @@ t.index ["tii_task_similarity_id"], name: "index_tii_submissions_on_tii_task_similarity_id" end - create_table "tutorial_enrolments", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "tutorial_enrolments", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "project_id", null: false @@ -404,7 +404,7 @@ t.index ["tutorial_id"], name: "index_tutorial_enrolments_on_tutorial_id" end - create_table "tutorial_streams", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "tutorial_streams", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name", null: false t.string "abbreviation", null: false t.datetime "created_at", null: false @@ -418,7 +418,7 @@ t.index ["unit_id"], name: "index_tutorial_streams_on_unit_id" end - create_table "tutorials", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "tutorials", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "unit_id" t.string "meeting_day" t.string "meeting_time" @@ -437,20 +437,21 @@ t.index ["unit_role_id"], name: "index_tutorials_on_unit_role_id" end - create_table "unit_roles", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "unit_roles", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "user_id" t.bigint "tutorial_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.bigint "role_id" t.bigint "unit_id" + t.boolean "observer", default: false t.index ["role_id"], name: "index_unit_roles_on_role_id" t.index ["tutorial_id"], name: "index_unit_roles_on_tutorial_id" t.index ["unit_id"], name: "index_unit_roles_on_unit_id" t.index ["user_id"], name: "index_unit_roles_on_user_id" end - create_table "units", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "units", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "name" t.string "description", limit: 4096 t.datetime "start_date" @@ -480,7 +481,7 @@ t.index ["teaching_period_id"], name: "index_units_on_teaching_period_id" end - create_table "users", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "users", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false t.string "reset_password_token" @@ -513,7 +514,7 @@ t.index ["role_id"], name: "index_users_on_role_id" end - create_table "webcal_unit_exclusions", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "webcal_unit_exclusions", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.bigint "webcal_id", null: false t.bigint "unit_id", null: false t.index ["unit_id", "webcal_id"], name: "index_webcal_unit_exclusions_on_unit_id_and_webcal_id", unique: true @@ -521,7 +522,7 @@ t.index ["webcal_id"], name: "fk_rails_d5fab02cb7" end - create_table "webcals", charset: "utf8", collation: "utf8_unicode_ci", force: :cascade do |t| + create_table "webcals", charset: "utf8mb3", collation: "utf8mb3_unicode_ci", force: :cascade do |t| t.string "guid", limit: 36, null: false t.boolean "include_start_dates", default: false, null: false t.bigint "user_id" diff --git a/test/helpers/authorisation_helpers_test.rb b/test/helpers/authorisation_helpers_test.rb new file mode 100644 index 000000000..628d574df --- /dev/null +++ b/test/helpers/authorisation_helpers_test.rb @@ -0,0 +1,61 @@ +require 'test_helper' + +class AuthorisationHelpersTest < ActiveSupport::TestCase + test "observer user cannot perform non-GET actions" do + role = Role.create!(name: 'Tutor') + user = User.new( + email: 'observer@example.com', + encrypted_password: 'password', + first_name: 'Observer', + last_name: 'User', + username: 'observer_user', + role: role + ) + user.save(validate: false) + + unit = Unit.create!( + name: 'Test Unit', + code: 'TST101', + description: 'Test unit description', + start_date: Date.today, + end_date: Date.today + 90.days + ) + + unit_role = UnitRole.new(user: user, unit: unit, role: role, observer: true) + unit_role.save(validate: false) + + unit.define_singleton_method(:role_for) { |_user| :tutor } + + result = AuthorisationHelpers.authorise?(user, unit_role, :post) + assert_equal false, result + end + + test "observer user can perform GET actions" do + role = Role.create!(name: 'Tutor') + user = User.new( + email: 'observer@example.com', + encrypted_password: 'password', + first_name: 'Observer', + last_name: 'User', + username: 'observer_user', + role: role + ) + user.save(validate: false) + + unit = Unit.create!( + name: 'Test Unit', + code: 'TST101', + description: 'Test unit description', + start_date: Date.today, + end_date: Date.today + 90.days + ) + + unit_role = UnitRole.new(user: user, unit: unit, role: role, observer: true) + unit_role.save(validate: false) + + unit.define_singleton_method(:role_for) { |_user| :tutor } + + result = AuthorisationHelpers.authorise?(user, unit_role, :get) + assert_equal true, result + end +end