Skip to content
Snippets Groups Projects
Commit 874d68a3 authored by Teal's avatar Teal
Browse files

Add RBAC, convert existing roles

parent 624f9615
No related branches found
No related tags found
1 merge request!20Add RBAC, convert existing roles
Pipeline #38220 passed
...@@ -6,13 +6,28 @@ class ApplicationController < ActionController::Base ...@@ -6,13 +6,28 @@ class ApplicationController < ActionController::Base
def configure_permitted_parameters def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [ :invitation_token ]) devise_parameter_sanitizer.permit(:sign_up, keys: [ :invitation_token ])
devise_parameter_sanitizer.permit(:account_update) do |u| devise_parameter_sanitizer.permit(:account_update) do |u|
u.permit(:name, :email, :password, :password_confirmation, :avatar_color, :darkmode, :languages_from, :languages_to, :telegram_username, :current_password) u.permit(:name, :email, :password, :password_confirmation, :avatar_color, :darkmode, :languages_from,
:languages_to, :telegram_username, :current_password)
end end
end end
def authorize_shiftcoordinator def authorize_shiftcoordinator
unless current_user.shiftcoordinator? authorize_role("shift_coordinator")
render plain: "Forbidden", status: :forbidden end
end
def redirect_back_with_error(message)
redirect_back(fallback_location: root_path, alert: message)
end
def authorize_role(role_name)
return if current_user&.has_role?(role_name)
render plain: "Forbidden", status: :forbidden
end
def authorize_permission(permission_name)
return if current_user&.has_permission?(permission_name)
render plain: "Forbidden", status: :forbidden
end end
end end
class Permission < ApplicationRecord
has_many :role_permissions, dependent: :destroy
has_many :roles, through: :role_permissions
validates :name, presence: true, uniqueness: { case_sensitive: false }
end
class Role < ApplicationRecord
has_many :user_roles, dependent: :destroy
has_many :users, through: :user_roles
has_many :role_permissions, dependent: :destroy
has_many :permissions, through: :role_permissions
validates :name, presence: true, uniqueness: { case_sensitive: false }
end
class RolePermission < ApplicationRecord
belongs_to :role
belongs_to :permission
end
...@@ -3,6 +3,9 @@ class User < ApplicationRecord ...@@ -3,6 +3,9 @@ class User < ApplicationRecord
has_many :assignments has_many :assignments
has_many :candidates has_many :candidates
has_many :user_roles, dependent: :destroy
has_many :roles, through: :user_roles
enum :darkmode, auto: 0, light: 1, dark: 2 enum :darkmode, auto: 0, light: 1, dark: 2
validates :darkmode, inclusion: { in: %w[auto light dark] } validates :darkmode, inclusion: { in: %w[auto light dark] }
...@@ -11,10 +14,12 @@ class User < ApplicationRecord ...@@ -11,10 +14,12 @@ class User < ApplicationRecord
validates :email, uniqueness: { case_sensitive: false, message: "already in use" }, allow_nil: true, allow_blank: true validates :email, uniqueness: { case_sensitive: false, message: "already in use" }, allow_nil: true, allow_blank: true
before_validation :cleanup_languages before_validation :cleanup_languages
validates :languages_from, format: { with: /\A([a-z][a-z])(,[a-z][a-z])*\z/, message: "please use comma-separated two-letter codes" }, allow_blank: true validates :languages_from,
format: { with: /\A([a-z][a-z])(,[a-z][a-z])*\z/, message: "please use comma-separated two-letter codes" }, allow_blank: true
validates :languages_from, length: { maximum: 14 } validates :languages_from, length: { maximum: 14 }
validates :languages_to, format: { with: /\A([a-z][a-z])(,[a-z][a-z])*\z/, message: "please use comma-separated two-letter codes" }, allow_blank: true validates :languages_to,
format: { with: /\A([a-z][a-z])(,[a-z][a-z])*\z/, message: "please use comma-separated two-letter codes" }, allow_blank: true
validates :languages_to, length: { maximum: 14 } validates :languages_to, length: { maximum: 14 }
validates :invitation_token, presence: true, on: :create validates :invitation_token, presence: true, on: :create
...@@ -35,9 +40,9 @@ class User < ApplicationRecord ...@@ -35,9 +40,9 @@ class User < ApplicationRecord
def self.leaderboard def self.leaderboard
all.map { |u| [ u.name, u.workload_minutes ] } all.map { |u| [ u.name, u.workload_minutes ] }
.sort_by { |_, workload| -workload } .sort_by { |_, workload| -workload }
.reject { |_, workload| workload.zero? } .reject { |_, workload| workload.zero? }
.to_h .to_h
end end
class Session < ApplicationRecord class Session < ApplicationRecord
...@@ -49,7 +54,6 @@ class User < ApplicationRecord ...@@ -49,7 +54,6 @@ class User < ApplicationRecord
end end
end end
def errors def errors
super.tap { |errors| errors.delete(:password, :blank) if password.nil? } super.tap { |errors| errors.delete(:password, :blank) if password.nil? }
end end
...@@ -73,7 +77,7 @@ class User < ApplicationRecord ...@@ -73,7 +77,7 @@ class User < ApplicationRecord
end end
def set_avatar_color def set_avatar_color
return unless self.avatar_color.nil? return unless avatar_color.nil?
r = rand(256) r = rand(256)
g = rand(256) g = rand(256)
...@@ -87,7 +91,19 @@ class User < ApplicationRecord ...@@ -87,7 +91,19 @@ class User < ApplicationRecord
end end
def workload_minutes def workload_minutes
Assignment.includes(:session).where(user: self).sum { | a | a.session.duration_minutes } Assignment.includes(:session).where(user: self).sum { |a| a.session.duration_minutes }
end
def has_role?(role_name)
roles.exists?(name: role_name)
end
def has_permission?(permission_name)
roles.joins(:permissions).exists?(permissions: { name: permission_name })
end
def shiftcoordinator?
has_role?("shift_coordinator")
end end
private private
...@@ -98,7 +114,7 @@ class User < ApplicationRecord ...@@ -98,7 +114,7 @@ class User < ApplicationRecord
end end
def cleanup_languages def cleanup_languages
self.languages_from = self.languages_from&.gsub(/\s+/, "")&.downcase self.languages_from = languages_from&.gsub(/\s+/, "")&.downcase
self.languages_to = self.languages_to&.gsub(/\s+/, "")&.downcase self.languages_to = languages_to&.gsub(/\s+/, "")&.downcase
end end
end end
class UserRole < ApplicationRecord
belongs_to :user
belongs_to :role
end
class CreateRoles < ActiveRecord::Migration[8.0]
def change
create_table :roles do |t|
t.string :name
t.text :description
t.timestamps
end
end
end
class CreateUserRoles < ActiveRecord::Migration[8.0]
def change
create_table :user_roles do |t|
t.references :user, null: false, foreign_key: true
t.references :role, null: false, foreign_key: true
t.timestamps
end
end
end
class CreatePermissions < ActiveRecord::Migration[8.0]
def change
create_table :permissions do |t|
t.string :name
t.text :description
t.timestamps
end
end
end
class CreateRolePermissions < ActiveRecord::Migration[8.0]
def change
create_table :role_permissions do |t|
t.references :role, null: false, foreign_key: true
t.references :permission, null: false, foreign_key: true
t.timestamps
end
end
end
class MigrateToRbacSystem < ActiveRecord::Migration[8.0]
def up
# Create roles
shift_coordinator_role = Role.create!(name: 'shift_coordinator', description: 'Can manage session assignments and scheduling')
events_admin_role = Role.create!(name: 'events_admin', description: 'Can manage conferences and all sub-resources')
# Create permissions
manage_assignments = Permission.create!(name: 'manage_assignments', description: 'Can create and delete assignments')
manage_conferences = Permission.create!(name: 'manage_conferences', description: 'Can create, edit, and delete conferences')
manage_sessions = Permission.create!(name: 'manage_sessions', description: 'Can create, edit, and delete sessions')
manage_speakers = Permission.create!(name: 'manage_speakers', description: 'Can create, edit, and delete speakers')
manage_stages = Permission.create!(name: 'manage_stages', description: 'Can create, edit, and delete stages')
# Associate permissions with roles
shift_coordinator_role.permissions << manage_assignments
events_admin_role.permissions << manage_conferences
events_admin_role.permissions << manage_sessions
events_admin_role.permissions << manage_speakers
events_admin_role.permissions << manage_stages
# Migrate existing shift coordinators to the new role system
User.where(shiftcoordinator: true).find_each do |user|
user.roles << shift_coordinator_role
puts "Migrated user #{user.name} to shift_coordinator role"
end
# Add a column to track migration completion
add_column :users, :migrated_to_rbac, :boolean, default: false
# Mark all users as migrated
User.update_all(migrated_to_rbac: true)
# Only remove the shiftcoordinator column after we are sure all users are migrated
# We'll do this in a separate migration after verifying
end
def down
# Restore shift coordinator status
if column_exists?(:users, :shiftcoordinator) && column_exists?(:users, :migrated_to_rbac)
User.where(migrated_to_rbac: true).find_each do |user|
user.update(shiftcoordinator: user.has_role?('shift_coordinator'))
end
end
# Remove the migration tracking column if it exists
remove_column :users, :migrated_to_rbac if column_exists?(:users, :migrated_to_rbac)
# No need to delete roles and permissions as they will be dropped with their tables
end
end
class RemoveShiftcoordinatorFromUsers < ActiveRecord::Migration[8.0]
def up
remove_column :users, :shiftcoordinator
end
def down
add_column :users, :shiftcoordinator, :boolean, default: false
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment