Reputation: 94
In one of my Rails (6.1.0) models I have a few columns that need to be protected from CRUD actions based on user role. I have implemented frontend form input disabling but not sure if I am protecting the backend correctly/safely. I experimented with a few ways and settled on this conditional concatenation on the params object, but since it's not something I have seen out there explicitly, I'm not confident it's best practice and/or fully safe. Is there a better way?
Given some user permission/access levels:
class User < ApplicationRecord
...
enum role: {guest: 0, employee: 1, manager: 2, executive: 3, administrator: 4, superadmin: 5, developer: 6}
#TODO: Extract & expand this authorization scheme (Pundit?)
def has_employee_rights?
self.employee? || self.manager? || self.executive? || self.administrator? || self.superadmin? || self.developer?
end
def has_manager_rights?
self.manager? || self.executive? || self.administrator? || self.superadmin? || self.developer?
end
def has_executive_rights?
self.executive? || self.administrator? || self.superadmin? || self.developer?
end
def has_administrator_rights?
self.administrator? || self.superadmin? || self.developer?
end
def has_superadmin_rights?
self.superadmin? || self.developer?
end
def has_developer_rights?
self.developer?
end
...
end
I have handled the front end form by setting the 'readonly' attribute (Haml w/ classes removed for clarity):
# Normal form field
= form.label :prov_dist_code, "Provisional Dist. Code"
= form.text_field :prov_dist_code, value: @project.prov_dist_code
# "Protected" form fields
= form.label :dist_code, "Final Distribution Code", readonly: !current_user.has_manager_rights?
= form.text_field :dist_code, value: @project.dist_code
= form.label :pcode, "PCode"
= form.text_field :pcode, readonly: !current_user.has_administrator_rights?, value: @project.pcode
Then in the controller I further protect the column:
class ProjectsController < ApplicationController
...
def create
@project = Project.new(project_params)
if @project.save
redirect_to @project, notice: "Project was successfully created."
else
render :new
end
end
...
private
#TODO: Is this conditional concatenation safe/adequate?
def allowed_params
ap = [:network_id,
:dist_title,
:dist_description,
:prov_dist_code,
:name,
:hosts,
:guests...]
ap << :pcode if current_user.has_administrator_rights?
ap << :dist_code if current_user.has_manager_rights?
end
def project_params
params.require(:project).permit(*allowed_params)
end
end
Upvotes: 0
Views: 294
Reputation: 102001
What you're doing is really just creating an array of arguments programatically and using the splat operator to apply the array as a list of arguments. There is nothing inherantly unsafe about that. The only real security concern would be if you're taking keys for the whitelist from the user.
From a code review standpoint through it does place a lot of logic in the controller layer where its hard to test and adds even more responsibilities to your model. Since you mentioned Pundit thats actually an excellent choice for where to consolidate your authorization logic as its easy to test in isolation.
class ApplicationPolicy < Pundit::Policy
# ...
private
def employee?
user.employee? || user.manager? || user.executive? || user.administrator? || user.superadmin? || user.developer?
end
def manager?
user.manager? || user.executive? || user.administrator? || user.superadmin? || user.developer?
end
def executive?
user.executive? || user.administrator? || user.superadmin? || user.developer?
end
def administrator?
user.administrator? || user.superadmin? || user.developer?
end
def superadmin?
user.superadmin? || user.developer?
end
def developer?
user.developer?
end
end
class ProjectPolicy < ApplicationPolicy
BASE_ATTRIBUTES = [
:network_id,
:dist_title,
:dist_description,
:prov_dist_code,
:name,
:hosts,
:guests
].freeze
# ...
def permitted_attributes
BASE_ATTRIBUTES.dup.then do |attrs|
attrs << :pcode if administrator?
attrs << :dist_code if manager?
end
end
end
You can then use your policy to whitelist the parameters:
class ProjectsController < ApplicationController
...
def create
@project = Project.new(permitted_attributes(@project))
if @project.save
redirect_to @project, notice: "Project was successfully created."
else
render :new
end
end
end
Upvotes: 1