diff --git a/Gemfile b/Gemfile index e9fd37cf..07cbd9b2 100644 --- a/Gemfile +++ b/Gemfile @@ -41,6 +41,7 @@ gem "flipper-ui" gem "httparty", "~> 0.21" gem "invisible_captcha" gem "omniauth-azure-activedirectory-v2" +gem "rolify" gem "sentry-rails", "~> 5.11" group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 7171c789..fc87f53e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -339,6 +339,7 @@ GEM actionpack (>= 5.2) railties (>= 5.2) rexml (3.2.6) + rolify (6.0.1) rspec-core (3.12.2) rspec-support (~> 3.12.0) rspec-expectations (3.12.3) @@ -499,6 +500,7 @@ DEPENDENCIES pry-byebug puma (~> 6.4) rails (~> 7.0.8) + rolify rspec-rails rubocop-govuk rubocop-performance diff --git a/app/controllers/system_admin/users_controller.rb b/app/controllers/system_admin/users_controller.rb index 86167fa5..3fb15920 100644 --- a/app/controllers/system_admin/users_controller.rb +++ b/app/controllers/system_admin/users_controller.rb @@ -8,12 +8,19 @@ def index def new @user = User.new + @roles = Role.all end - def edit; end + def edit + @roles = Role.all + end def create @user = User.new(user_params) + @user.roles = [] + params[:user][:role_ids].each do |role_id| + @user.add_role(Role.find(role_id).name) if role_id.present? + end if @user.save redirect_to(users_path, notice: t("users.create.success")) @@ -24,6 +31,11 @@ def create def update if @user.update(user_params) + @user.roles = [] + params[:user][:role_ids].each do |role_id| + @user.add_role(Role.find(role_id).name) if role_id.present? + end + @user.save redirect_to(users_path, notice: t("users.update.success")) else render(:edit, status: :unprocessable_entity) @@ -45,7 +57,7 @@ def set_user # Only allow a list of trusted parameters through. def user_params - params.require(:user).permit(:email) + params.require(:user).permit(:email, :role_ids) end end end diff --git a/app/models/role.rb b/app/models/role.rb new file mode 100644 index 00000000..a6c51029 --- /dev/null +++ b/app/models/role.rb @@ -0,0 +1,26 @@ +# == Schema Information +# +# Table name: roles +# +# id :bigint not null, primary key +# name :string +# resource_type :string +# created_at :datetime not null +# updated_at :datetime not null +# resource_id :bigint +# +class Role < ApplicationRecord + ROLES_LIST = %i[spectator servant manager admin super_admin].freeze + + has_many :users, through: :users_roles + + belongs_to :resource, + polymorphic: true, + optional: true + + validates :resource_type, + inclusion: { in: Rolify.resource_types }, + allow_nil: true + + scopify +end diff --git a/app/models/user.rb b/app/models/user.rb index 651f8a11..4b336863 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -8,6 +8,7 @@ # updated_at :datetime not null # class User < ApplicationRecord + rolify audited devise :omniauthable, omniauth_providers: %i[azure_activedirectory_v2] end diff --git a/app/views/system_admin/users/_form.html.erb b/app/views/system_admin/users/_form.html.erb index 34dfaaf2..44eebb8a 100644 --- a/app/views/system_admin/users/_form.html.erb +++ b/app/views/system_admin/users/_form.html.erb @@ -2,6 +2,7 @@ <%= f.govuk_error_summary %> <%= f.govuk_text_field :email, label: { text: 'Email' }, hint: { text: 'The email address of the user' } %> + <%= f.govuk_collection_check_boxes :role_ids, @roles, :id, ->(r){r.name.humanize}, legend: { text: "Roles" } %> <%= f.govuk_submit("Save") %> <% end %> diff --git a/config/initializers/rolify.rb b/config/initializers/rolify.rb new file mode 100644 index 00000000..da8907ef --- /dev/null +++ b/config/initializers/rolify.rb @@ -0,0 +1,10 @@ +Rolify.configure do |config| + # By default ORM adapter is ActiveRecord. uncomment to use mongoid + # config.use_mongoid + + # Dynamic shortcuts for User class (user.is_admin? like methods). Default is: false + # config.use_dynamic_shortcuts + + # Configuration to remove roles from database once the last resource is removed. Default is: true + # config.remove_role_if_empty = false +end diff --git a/db/migrate/20231003024901_rolify_create_roles.rb b/db/migrate/20231003024901_rolify_create_roles.rb new file mode 100644 index 00000000..e7ca9c07 --- /dev/null +++ b/db/migrate/20231003024901_rolify_create_roles.rb @@ -0,0 +1,18 @@ +class RolifyCreateRoles < ActiveRecord::Migration[7.0] + def change + create_table(:roles) do |t| + t.string :name + t.references :resource, polymorphic: true + + t.timestamps + end + + create_table(:users_roles, id: false) do |t| + t.references :user + t.references :role + end + + add_index(:roles, %i[name resource_type resource_id]) + add_index(:users_roles, %i[user_id role_id]) + end +end diff --git a/db/schema.rb b/db/schema.rb index 4d5e00a1..e7f52481 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,8 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_10_02_115920) do + +ActiveRecord::Schema[7.0].define(version: 2023_10_03_024901) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "plpgsql" @@ -176,6 +177,16 @@ t.index ["application_id"], name: "index_qa_statuses_on_application_id" end + create_table "roles", force: :cascade do |t| + t.string "name" + t.string "resource_type" + t.bigint "resource_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["name", "resource_type", "resource_id"], name: "index_roles_on_name_and_resource_type_and_resource_id" + t.index ["resource_type", "resource_id"], name: "index_roles_on_resource" + end + create_table "schools", force: :cascade do |t| t.string "name" t.string "headteacher_name" @@ -198,6 +209,14 @@ t.datetime "updated_at", null: false end + create_table "users_roles", id: false, force: :cascade do |t| + t.bigint "user_id" + t.bigint "role_id" + t.index ["role_id"], name: "index_users_roles_on_role_id" + t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id" + t.index ["user_id"], name: "index_users_roles_on_user_id" + end + add_foreign_key "applicants", "schools" add_foreign_key "applications", "applicants" add_foreign_key "qa_statuses", "applications" diff --git a/lib/tasks/roles.rake b/lib/tasks/roles.rake new file mode 100644 index 00000000..60a92268 --- /dev/null +++ b/lib/tasks/roles.rake @@ -0,0 +1,8 @@ +namespace :roles do + desc "Create default roles" + task create_defaults: :environment do + Role::ROLES_LIST.each do |role_name| + Role.find_or_create_by(name: role_name) + end + end +end diff --git a/spec/factories/roles.rb b/spec/factories/roles.rb new file mode 100644 index 00000000..93fe95f7 --- /dev/null +++ b/spec/factories/roles.rb @@ -0,0 +1,16 @@ +# == Schema Information +# +# Table name: roles +# +# id :bigint not null, primary key +# name :string +# resource_type :string +# created_at :datetime not null +# updated_at :datetime not null +# resource_id :bigint +# +FactoryBot.define do + factory :role do + name { "Admin" } + end +end diff --git a/spec/models/role_spec.rb b/spec/models/role_spec.rb new file mode 100644 index 00000000..a9cd1e51 --- /dev/null +++ b/spec/models/role_spec.rb @@ -0,0 +1,16 @@ +# == Schema Information +# +# Table name: roles +# +# id :bigint not null, primary key +# name :string +# resource_type :string +# created_at :datetime not null +# updated_at :datetime not null +# resource_id :bigint +# +require "rails_helper" + +RSpec.describe Role do + it { is_expected.to have_db_column(:name) } +end