In an API controller, I'd like to limit what fields of a model can be seen depending on who is logged in. ActiveModel Serializers would seem to allow this, but I've had no luck with the following:
class MyModelSerializer < ActiveModel::Serializer
attributes :name, :custom_field, :secret_field
has_many :linked_records
def custom_field
object.do_something
end
def filter(keys)
unless scope.is_admin?
keys.delete :secret_field
keys.delete :linked_records
end
keys
end
end
But, the filtering is never performed and so my output always contains :secret_field and :linked_records even if there's no user logged in.
Perhaps this is because I am using Rails 6, and it would seem that ActiveModel Serializers might no longer be the best tool (e.g. https://stevenyue.com/blogs/migrating-active-model-serializers-to-jserializer).
Please do offer your suggestions for a means to perform this, if you can think of a better means.
EDIT:
Further to all the comments below, here's some different code:
attributes :name, :id, :admin_only_field, :is_admin
$admin_only = %i[:id, :admin_only_field]
def attributes(*args)
hash = super
$admin_only.each do |key|
unless scope.is_admin?
hash.delete(key)
end
end
hash
end
def is_admin
if scope.is_admin?
'admin!'
else
'not an admin!'
end
end
If I then visit the model's index page without being an admin I see that the admin_only_field and id are both present, and is_admin says that I'm not. Bizarre.
class MyModelSerializer < ActiveModel::Serializer
attributes :name, :custom_field, :secret_field
has_many :linked_records
def custom_field
object.do_something
end
private
def attributes
hash = super
unless scope.is_admin?
hash.delete :secret_field
hash.delete :linked_records
end
hash
end
end
Related
Here is my code so far:
class Video < ActiveRecord::Base
has_paper_trail meta: { athlete_id: :athlete_id, approved: false },
if: Proc.new { |v| v.needs_approval? }
validate :should_be_saved?
def should_be_saved?
errors.add(:base, 'added for approval') if needs_approval?
end
def needs_approval
#needs_approval ||= false
end
def needs_approval?
#needs_approval
end
end
# ApplicationController
class ApplicationController < ActionController::Base
def user_for_paper_trail
return unless user_signed_in?
original_user.present? ? original_user : current_user
end
# Used to determine the contributor
# When contributor logs in, warden saves the contributor_id in session
def original_user
return nil unless remember_contributor_id?
#original_user ||= User.find(remember_contributor_id)
end
def info_for_paper_trail
{ athlete_id: current_user.id } if current_user
end
end
The problem I am running into currently is when the Video object is saved the validation fails (because I told it too), but I need for the validation to fail but the version object continue with its creation. Just not too sure how to go about doing that.
EDITS
Here is my code (the code below is still using the ApplicationController code from above):
class Video < ActiveRecord::Base
# .. other methods
include Contributable
attr_accessible :video_type_id, :athlete_id, :uploader_id, :created_at, :updated_at, :uniform_number, :featured,
:name, :panda_id, :date, :thumbnail_url, :mp4_video_url, :from_mobile_device, :duration, :sport_id,
:delted_at, :approved
end
module Contributable
extend ActiveSupport::Concern
included do
has_paper_trail meta: { athlete_id: :athlete_id, approved: false },
unless: Proc.new { |obj| obj.approved? },
skip: [:approved]
end
def log_changes_or_update(params, contributor = nil)
update_attribute(:approved, false) unless contributor.blank?
if contributor.blank?
update_attributes params
else
self.attributes = params
self.send(:record_update)
self.versions.map(&:save)
end
end
end
class VideosController < ApplicationController
def update
# ... other code
# original_user is the contributor currently logged in
#video.log_changes_or_update(params[:video], original_user)
end
end
The app I am working on has a small layer of complexity that allows for users with a certain role to edit profiles they have access too. I am trying to save the versions of each change out (using paper_trail) without affecting the existing object.
The code above works exactly how I want it to, however, I am just curious to know if in my log_changes_or_update method is not the correct way to go about accomplishing the overall goal.
Why not just remove the validation and add an approved attribute with a default value of false to the Video model? That way the Video object is saved and a paper_trail version is created. Later when the video gets approved paper_trail will note that change too.
The title says everything. I currently have a model concern with which I'm setting if a model can have attachments or not using include Attachable. So far so good.
Then, when I display the list of files attached to a particular model, I'm adding a link to delete it such as :
DELETE /posts/:post_id/attachments/:id(.:format) attachments#destroy
For that purpose, I created an AttachmentsController with a destroy method. So I have 2 problems here. The first, how can I delete a file from this controller using Carrierwave (for delete the file itself and the table record)?
Second, since my attachable behavior gonna be plug in several model :
DELETE /posts/:post_id/attachments/:id(.:format) attachments#destroy
DELETE /users/:user_id/attachments/:id(.:format) attachments#destroy
...
How can I do in my AttachmentsController to delete a file depending on the associated model dynamically?
class Attachment < ActiveRecord::Base
include Sluggable
belongs_to :attachable, polymorphic: true
mount_uploader :file, AttachmentUploader
validates :name, presence: true, if: :file?
validates :file, presence: true, if: :name?
end
class AttachmentsController < ApplicationController
before_action :authenticate_user!
def destroy
// Don't know how to remove that file
redirect_to :back
rescue ActionController::RedirectBackError
redirect_to root_path
end
end
Hope I was clear.
Thanks
EDIT :
Ok I create a tweak on the params hash in order to get the associated object dynamically within AttachmentsController :
private
def get_attachable_model
params.each do |name, value|
if name =~ /(.+)_id$/
model = name.match(/([^\/.]*)_id$/)
return model[1].classify.constantize
end
end
nil
end
Ok I finally found a solution myself. Here is my destroy method from AttachmentsController :
def destroy
model, param = get_attachable_instance
model_attach = model.find_by slug: params[param.to_sym]
file = model_attach.attachments.find_by slug: params[:id]
file.destroy
redirect_to :back
rescue ActionController::RedirectBackError
redirect_to root_path
end
Not sure if it's the best way to go, but it does works
So I have some data that gets pulled from another rails app in a controller lets call it ExampleController and I want to validate it as being there in my model before allowing the wizard to move to its next step and I can't quite figure out how I should be doing it (I know that getting this data directly from the controller into the model violates MVC I am looking for the best workaround to get my data from the controller) . The data must come from the controller as the methods for getting it are contained in ApplicationController however I could do this in the Awizard controller if this is easier. (Also I cannot use a gem)
Please offer some kind of suggestion to the problem and not an explanation of why this is not the correct way to do things I realise that already but cannot do it another way.
The Example Controller
should this instead render the data then check it isn't blank elsewhere?
class ExampleController < ApplicationController
def valid_data?
data = #data could be nil or not
if data.blank?
return false
else
return true
end
end
My Model - (models/awizard.rb)
How do I use the valid_data? method from the example controller? in my validation here.
class AWizard
include ActiveModel::Validations
include ActiveModel::Conversion
include ActiveModel::Dirty
include ActiveModel::Naming
#This class is used to manage the wizard steps using ActiveModel (not ActiveRecord)
attr_accessor :id
attr_writer :current_step #used to write to current step
define_attribute_methods [:current_step] #used for marking change
validate :first_step_data, :if => lambda { |o| o.current_step == "step1" };
def first_step_data
#What should i put here to check the valid_data? from the examplecontroller
end
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
def current_step
#current_step || steps.first
end
def steps
%w[step1 step2 step3] #make list of steps (partials)
end
def next_step
current_step_will_change! #mark changed when moving stepped
self.current_step = steps[steps.index(current_step)+1] unless last_step?
end
def previous_step
current_step_will_change! #mark changed when moving stepped
self.current_step = steps[steps.index(current_step)-1] unless first_step?
end
def first_step?
current_step == steps.first
end
def last_step?
current_step == steps.last
end
def all_valid?
steps.all? do |step|
self.current_step = step
valid?
end
end
def step(val)
current_step_will_change!
self.current_step = steps[val]
end
def persisted?
self.id == 1
end
end
Or do I need to add this to this view?
(/views/awizard/_step1.html.erb)
<div class="field">
<%= f.label 'Step1' %><br />
#This is the step I want to validate
</div>
I maybe have misunderstood the question since my answer is simple. However here's a solution that doesn't resort to metaprogramming, but to the fact that Wizard (the class not objects it creates ) is a singleton/constant.
class ExampleController < ApplicationController
def valid_data?
data = #data could be nil or not
result = data.blank?
Awizard.valid_data= result
result
end
end
class Wizard
cattr_accessor :valid_data
def valid_data?
self.class.valid_data
end
end
If course ExampleController#valid_data must have been called before you play around with a Wizard passing step_one.
UPDATE:Reasoning about the global state problem
(raised by #Valery Kvon)
The argument is that Wizard is global to the application and that #wizard instances will be dependant on a global state and are therefore badly encapsulated. But Data, coming from another site, is gloabl in the scope of your app. So there's no mismatch with Wizard beeing the one holding the data. On the contrary it can be considered as a feature.
One example. Wizards magic is only efficient at full moon.
Application SkyReport sends data :
:full_moon => true
It affects all wizards in stage 1 if they need to go on step2 of their power. Therefore relying on the global state of Wizard.valid_data? is exactly what we want...
However if each wizard has a personal message coming from Gandalf's application, then we'll want to inforce the invocation of Gandalf's data but then the solution is even simpler :
# in example_controller.rb
before_filter :set_wizard_data, :only => [:create, :update]
....
def set_wizard_data
#wizard = Wizard.find params[:id]
#wizard.valid_data= valid_data
end
But this again implies that Gandalf.app knows (something of) the #wizard and from how the problem is presented, data coming from the other site is pretty agnostic !
The issue here is that we don't know enough about the app, its requirements and underlying logic to decide what's good or not...
The only way to share controller level data with model is through external accessor.
Using metaprogramming you can trick the way to pass it to a model instance.
controller
def valid_data?
data = #data could be nil or not
result = data.blank? ? false : true
instance_eval <<-EOV
def AWizard.new(*args)
super(*args).tap {|aw| aw.external_valid = #{result}}
end
EOV
result
end
model
class AWizard
attr_accessor :external_valid
def initialize(attributes = {})
attributes.each do |name, value|
send("#{name}=", value)
end
end
validate :first_step_data, :if => lambda { |o| o.current_step == "step1" };
def first_step_data
# :external_valid would be true or false according to a valid_data?. Nil would be if valid_data? has not been called
if external_valid == false
errors.add ...
end
end
end
You can pass the data from the controller as a parameter to the validation method in the model.
In your models/awizard.rb
def valid_for_step_one?(some_external_data)
#validation logic here
end
So that in your controller, you can can call:
model.valid_for_step_one?(data_from_controller)
It would also be good if you can give a description of the data you are getting from the controller. How is it related to the model awizard?
Because another option is to set the external data as an attribute of the model. Then you can use it in your validation functions from there.
So I tried to edit and add to the #charlysisto question as this was closest to the answer but it did not work so here is the solution I used, as suggested the answer was to send the data from the controller to the model (Although the answers left out using the view to call the controller method) here is my solution
Model - models/awizard.rb
class Awizard
include ActiveModel::Validations
cattr_accessor :valid_data
validate :data_validation :if => lambda { |o| o.current_step == "step1" }
def data_validation
if self.valid_data == false || self.valid_data.blank?
errors.add(:valid_data, "not found")
end
end
#Other wizard stuff
end
View - awizard/_step1.html.erb
<div class="field">
<% f.label "valid data? %>
<% #_controller.valid_data %> #Call controller method to send data to model
</div>
Controller
class AwizardController < ApplicationController
def valid_data
data = #data from elsewhere
if !data.blank?
Awizard.valid_data = true
else
Awizard.valid_data = false
end
end
For some reason #store object with three attributes won't assign the third value(generated token string).The model has user_id, product_id and token_string.But when it comes to token_generate function nothing is assigned,and end up with nil in the token_string column in the database .Product_id and user_id are saved perfectly in the database though.What is going on here?Thank you in advance.
class Store < ActiveRecord::Base
require 'digest/sha1'
attr_accessor :token_string
before_save :token_generate
def save_with_payment
#Until here the object has user_id and product_id attribute values
save!
end
private
def token_generate
self.token_string = Digest::SHA1.hexdigest("random string")
end
end
controller
def create
#store=Store.new(params[:store])
if #store.save_with_payment
redirect_to :controller=>"products",:action=>"index"
else
redirect_to :action=>"new"
end
end
#SrdjanPejic is correct, try removing the attr_accessor :token_string line which is likely blocking setting the :attributes hash value needed for the INSERT statement.
What's a cool way to protect attributes by role using declarative_authorization? For example, a user can edit his contact information but not his role.
My first inclination was to create multiple controller actions for different scenarios. I quickly realized how unwieldy this could become as the number of protected attributes grows. Doing this for user role is one thing, but I can imagine multiple protected attributes. Adding a lot controller actions and routes doesn't feel right.
My second inclination was to create permissions around specific sensitive attributes and then wrap the form elements with View hepers provided by declarative_authorizations. However, the model and controller aspect of this is a bit foggy in my mind. Suggestions would be awesome.
Please advise on the best way to protect attributes by role using declarative_authorizations.
EDIT 2011-05-22
Something similar is now in Rails as of 3.1RC https://github.com/rails/rails/blob/master/activerecord/test/cases/mass_assignment_security_test.rb so I would suggest going that route now.
ORIGINAL ANSWER
I just had to port what I had been using previously to Rails 3. I've never used declarative authorization specifically, but this is pretty simple and straightforward enough that you should be able to adapt to it.
Rails 3 added mass_assignment_authorizer, which makes this all really simple. I used that linked tutorial as a basis and just made it fit my domain model better, with class inheritance and grouping the attributes into roles.
In model
acts_as_accessible :admin => :all, :moderator => [:is_spam, :is_featured]
attr_accessible :title, :body # :admin, :moderator, and anyone else can set these
In controller
post.accessed_by(current_user.roles.collect(&:code)) # or however yours works
post.attributes = params[:post]
lib/active_record/acts_as_accessible.rb
# A way to have different attr_accessible attributes based on a Role
# #see ActsAsAccessible::ActMethods#acts_as_accessible
module ActiveRecord
module ActsAsAccessible
module ActMethods
# In model
# acts_as_accessible :admin => :all, :moderator => [:is_spam]
# attr_accessible :title, :body
#
# In controller
# post.accessed_by(current_user.roles.collect(&:code))
# post.attributes = params[:post]
#
# Warning: This frequently wouldn't be the concern of the model where this is declared in,
# but it is so much more useful to have it in there with the attr_accessible declaration.
# OHWELL.
#
# #param [Hash] roles Hash of { :role => [:attr, :attr] }
# #see acts_as_accessible_attributes
def acts_as_accessible(*roles)
roles_attributes_hash = Hash.new {|h,k| h[k] ||= [] }
roles_attributes_hash = roles_attributes_hash.merge(roles.extract_options!).symbolize_keys
if !self.respond_to? :acts_as_accessible_attributes
attr_accessible
write_inheritable_attribute :acts_as_accessible_attributes, roles_attributes_hash.symbolize_keys
class_inheritable_reader :acts_as_accessible_attributes
# extend ClassMethods unless (class << self; included_modules; end).include?(ClassMethods)
include InstanceMethods unless included_modules.include?(InstanceMethods)
else # subclass
new_acts_as_accessible_attributes = self.acts_as_accessible_attributes.dup
roles_attributes_hash.each do |role,attrs|
new_acts_as_accessible_attributes[role] += attrs
end
write_inheritable_attribute :acts_as_accessible_attributes, new_acts_as_accessible_attributes.symbolize_keys
end
end
end
module InstanceMethods
# #param [Array, NilClass] roles Array of Roles or nil to reset
# #return [Array, NilClass]
def accessed_by(*roles)
if roles.any?
case roles.first
when NilClass
#accessed_by = nil
when Array
#accessed_by = roles.first.flatten.collect(&:to_sym)
else
#accessed_by = roles.flatten.flatten.collect(&:to_sym)
end
end
#accessed_by
end
private
# This is what really does the work in attr_accessible/attr_protected.
# This override adds the acts_as_accessible_attributes for the current accessed_by roles.
# #see http://asciicasts.com/episodes/237-dynamic-attr-accessible
def mass_assignment_authorizer
attrs = []
if self.accessed_by
self.accessed_by.each do |role|
if self.acts_as_accessible_attributes.include? role
if self.acts_as_accessible_attributes[role] == :all
return self.class.protected_attributes
else
attrs += self.acts_as_accessible_attributes[role]
end
end
end
end
super + attrs
end
end
end
end
ActiveRecord::Base.send(:extend, ActiveRecord::ActsAsAccessible::ActMethods)
spec/lib/active_record/acts_as_accessible.rb
require 'spec_helper'
class TestActsAsAccessible
include ActiveModel::MassAssignmentSecurity
extend ActiveRecord::ActsAsAccessible::ActMethods
attr_accessor :foo, :bar, :baz, :qux
acts_as_accessible :dude => [:bar], :bra => [:baz, :qux], :admin => :all
attr_accessible :foo
def attributes=(values)
sanitize_for_mass_assignment(values).each do |k, v|
send("#{k}=", v)
end
end
end
describe TestActsAsAccessible do
it "should still allow mass assignment to accessible attributes by default" do
subject.attributes = {:foo => 'fooo'}
subject.foo.should == 'fooo'
end
it "should not allow mass assignment to non-accessible attributes by default" do
subject.attributes = {:bar => 'baaar'}
subject.bar.should be_nil
end
it "should allow mass assignment to acts_as_accessible attributes when passed appropriate accessed_by" do
subject.accessed_by :dude
subject.attributes = {:bar => 'baaar'}
subject.bar.should == 'baaar'
end
it "should allow mass assignment to multiple acts_as_accessible attributes when passed appropriate accessed_by" do
subject.accessed_by :bra
subject.attributes = {:baz => 'baaaz', :qux => 'quuux'}
subject.baz.should == 'baaaz'
subject.qux.should == 'quuux'
end
it "should allow multiple accessed_by to be specified" do
subject.accessed_by :dude, :bra
subject.attributes = {:bar => 'baaar', :baz => 'baaaz', :qux => 'quuux'}
subject.bar.should == 'baaar'
subject.baz.should == 'baaaz'
subject.qux.should == 'quuux'
end
it "should allow :all access" do
subject.accessed_by :admin
subject.attributes = {:bar => 'baaar', :baz => 'baaaz', :qux => 'quuux'}
subject.bar.should == 'baaar'
subject.baz.should == 'baaaz'
subject.qux.should == 'quuux'
end
end
To me this filtering problem is something that should be applied at the controller level.
You'll want to have something somewhere that defines how to decide which attributes are writeable for a given user.
# On the user model
class User < ActiveRecord::Base
# ...
# Return a list of symbols representing the accessible attributes
def self.allowed_params(user)
if user.admin?
[:name, :email, :role]
else
[:name, email]
end
end
end
Then, in the application controller you can define a method to filter parameters.
class ApplicationController < ActionController::Base
# ...
protected
def restrict_params(param, model, user)
params[param].reject! do |k,v|
!model.allowed_params(user).include?(k)
end
end
# ...
end
And finally in your controller action you can use this filter:
class UserController < ActionController::Base
# ...
def update
restrict_params(:user, User, #current_user)
# and continue as normal
end
# ...
end
The idea is that you could then define allowed_params on each of your models, and have the controllers for each of these use the same filter method. You could save some boilerplate by having a method in application controller that spits out a before filter, like this:
def self.param_restrictions(param, model)
before_filter do
restrict_params(param, model, #current_user) if params[param]
end
end
# in UserController
param_restrictions :user, User
These examples are intended to be illustrative rather than definitive, I hope they help with the implementation of this.
I'd use scoped_attr_accessible, which looks like just what you're looking for. Only you need to set the scope at the start of a request for all models.
To do that, use a before_filter in your application_controller.rb:
before_filter do |controller|
ScopedAttrAccessible.current_sanitizer_scope = controller.current_user.role
end
I would avoid every solution based on user access in model because it seems potentially dangerous. I would try this approach:
class User < ActiveRecord::Base
def update_attributes_as_user(values, user)
values.each do |attribute, value|
# Update the attribute if the user is allowed to
#user.send("#{attribute}=", value) if user.modifiable_attributes.include?(attribute)
end
save
end
def modifiable_attributes
admin? ? [:name, :email, :role] : [:name, :email]
end
end
Then in your controller change your update action from:
#user.update_attributes(params[:user])
to
#user.update_attributes_as_user(params[:user], current_user)
Rails 3.1+ comes with a +assign_attributes+ method for this purpose - http://apidock.com/rails/ActiveRecord/AttributeAssignment/assign_attributes.