I'm trying to set default scope according to some criteria determined by ana ActionController before_filter. In controller:
before_filter :authorize
...
def authorize
if some_condition
#default_scope_conditions = something
elsif another_condition
#default_scope_conditions = something_else
end
end
Inside the ActiveRecord
default_scope :conditions => #default_scope_conditions
But it doesn't seem to work, the before filter gets called but the default_scope doesn't get set. Could you please advise me what I'm doing wrong and how to fix it or suggest me some other way of achieving that.
You set #default_scope_conditions - which is an instance variable from the controller and you expect to read it from the model. It is not visible from the model unless passed as method parameter.
More, this approach would break the MVC principle that separates the model logic from the controller logic: Your model shouldn't automatically access info about current state of the controller.
What you can do: use anonymous scopes.
def scope_user
if some_condition
#default_scope_conditions = something
elsif another_condition
#default_scope_conditions = something_else
end
#user_scoped = User.scoped(:conditions => #default_scope_conditions)
end
Then, in your method, you can:
def my_method
users = #user_scoped.all
end
or
def my_method
User.with_scope(:conditions => #default_scope_conditions) do
# ..
#users = User.all #users get scoped
#products.users # also gets scoped
end
end
Try one default_scope and override it using custome finder.
The default options can always be overridden using a custom finder.
class User < ActiveRecord::Base
default_scope :order => '`users`.name asc'
end
User.all # will use default scope
User.all(:order => 'name desc') # will use passed in order option.
Then you can try something like following
before_filter :authorize
...
def authorize
if some_condition
#order = '' # default scope
elsif another_condition
#order = something_else
end
end
def method_name
User.all(:order => #order)
end
No Check though.
Related
I am trying to figure out how to write pundit permissions in my Rails 4 app.
I have an article model, with an article policy. The article policy has:
class ArticlePolicy < ApplicationPolicy
attr_reader :user, :scope
def initialize(user, record, scope)
#scope = scope
super(user, record)
end
class Scope < Scope
def resolve
if user == article.user
scope.where(user_id: user_id)
elsif approval_required?
scope.where(article.state_machine.in_state?(:review)).(user.has_role?(:org_approver))
else
article.state_machine.in_state?(:publish)
end
end
end
# TO DO - check if this is correct - I'm not sure.
# I now think I don't need the index actions because I have changed the action in the articles controller to look for policy scope.
# def index?
# article.state_machine.in_state?(:publish)
# end
def article
record
end
The articles controller has:
def index
#articles = policy_scope(Article)
# query = params[:query].presence || "*"
# #articles = Article.search(query)
end
I am following the pundit documents relating to scopes and trying to figure out why the index action shown in the policy documents isn't working for me. I have tried the following (as shown in the docs):
<% policy_scope(#user.articles).sort_by(&:created_at).in_groups_of(2) do |group| %>
but I get this error:
undefined local variable or method `article' for #<ArticlePolicy::Scope:0x007fff08ae9f48>
Can anyone see where I've gone wrong?
I'm not sure that #user.articles is right. In my construct, articles belong to users, but in my index action, I want to show every user the articles that my scopes allow them to see.
You can try this in your action in controller.
#articles = policy_scope(Article).all
It will get all the articles. If you want to get the articles based on search params, you can try this.
#q = policy_scope(Article).search(params[:query])
#articles = #q.result
I think you may need to explicitly set article as an accessor in the Scope class as the error indicates that it doesn't recognise 'article'. Try something like
attr_accessor :article
set it in an initialize method and you can probably do away with the article method.
def initialize(record)
#article = record
end
I want to create a default scope to filter all queries depending on the current user. Is it possible to pass the current user as an argument to the default_scope? (I know this can be done with regular scopes) If not, what would be another solution?
Instead of using default_scope which has a few pitfalls, you should consider using a named scope with a lambda. For example scope :by_user, -> (user) { where('user_id = ?', user.id) }
You can then use a before_filter in your controllers to easily use this scope in all the actions you need.
This is also the proper way to do it since you won't have access to helper methods in your model. Your models should never have to worry about session data, either.
Edit: how to use the scope in before_filter inside a controller:
before_filter :set_object, only: [:show, :edit, :update, :destroy]
[the rest of your controller code here]
private
def set_object
#object = Object.by_user(current_user)
end
obviously you'd change this depending on your requirements. Here we're assuming you only need a valid #object depending on current_user inside your show, edit, update, and destroy actions
You cannot pass an argument to a default scope, but you can have a default scope's conditions referencing a proxy hash that executes its procs every time a value is retrieved:
module Proxies
# ProxyHash can be used when there is a need to have procs inside hashes, as it executes all procs before returning the hash
# ==== Example
# default_scope(:conditions => Proxies::ProxyHash.new(:user_id => lambda{Thread.current[:current_user].try(:id)}))
class ProxyHash < ::Hash
instance_methods.each{|m| undef_method m unless m =~ /(^__|^nil\?$|^method_missing$|^object_id$|proxy_|^respond_to\?$|^send$)/}
def [](_key)
call_hash_procs(#target, #original_hash)
ProxyHash.new(#original_hash[_key])
end
# Returns the \target of the proxy, same as +target+.
def proxy_target
#target
end
# Does the proxy or its \target respond to +symbol+?
def respond_to?(*args)
super(*args) || #target.respond_to?(*args)
end
# Returns the target of this proxy, same as +proxy_target+.
def target
#target
end
# Sets the target of this proxy to <tt>\target</tt>.
def target=(target)
#target = target
end
def send(method, *args)
if respond_to?(method)
super
else
#target.send(method, *args)
end
end
def initialize(*_find_args)
#original_hash = _find_args.extract_options!
#target = #original_hash.deep_dup
end
private
# Forwards any missing method call to the \target.
def method_missing(method, *args, &block)
if #target.respond_to?(method)
call_hash_procs(#target, #original_hash)
#target.send(method, *args, &block)
else
super
end
end
def call_hash_procs(_hash, _original_hash)
_hash.each do |_key, _value|
if _value.is_a?(Hash)
call_hash_procs(_value, _original_hash[_key]) if _original_hash.has_key?(_key)
else
_hash[_key] = _original_hash[_key].call if _original_hash[_key].is_a?(Proc)
end
end
end
end
end
Then in ApplicationController you can use an around_filter to set/unset Thread.current[:current_user] at the begin/end of each request:
class ApplicationController < ActionController::Base
around_filter :set_unset_current_user
protected
def set_unset_current_user
Thread.current[:current_user] = current_user if logged_in?
yield
ensure
Thread.current[:current_user] = nil
end
end
I have a Log model that belongs to User and Firm. For setting this I have this code in the logs_controller's create action.
def create
#log = Log.new(params[:log])
#log.user = current_user
#log.firm = current_firm
#log.save
end
current_user and current_firm are helper methods from the application_helper.rb
While this works it makes the controller fat. How can I move this to the model?
I believe this sort of functionality belongs in a 'worker' class in lib/. My action method might look like
def create
#log = LogWorker.create(params[:log], current_user, current_firm)
end
And then I'd have a module in lib/log_worker.rb like
module LogWorker
extend self
def create(params, user, firm)
log = Log.new(params)
log.user = user
log.firm = firm
log.save
end
end
This is a simplified example; I typically namespace everything, so my method might actually be in MyApp::Log::Manager.create(...)
No difference: You can refactor the code:
def create
#log = Log.new(params[:log].merge(:user => current_user, :firm => current_firm)
#log.save
end
And your Log have to:
attr_accessible :user, :firm
Not much shorter, but the responsibility for the handling of current_user falls to the controller in MVC
def create
#log = Log.create(params[:log].merge(
:user => current_user,
:firm => current_firm))
end
EDIT
If you don't mind violating MVC a bit, here's a way to do it:
# application_controller.rb
before_filter :set_current
def set_current
User.current = current_user
Firm.current = current_firm
end
# app/models/user.rb
cattr_accessor :current
# app/models/firm.rb
cattr_accessor :current
# app/models/log.rb
before_save :set_current
def set_current
self.firm = Firm.current
self.user = User.current
end
# app/controllers/log_controller.rb
def create
#log = Log.create(params[:log])
end
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.
I want some thing like below
class CompanyController < ApplicationController
def index
#return all of companies
end
def index
#return companies based filter on company :name, :location, :type (any combination of these)
end
end
You can't do that but you can do something like this:
class CompanyController < ApplicationController
def index
if params[:name] # add ifs etc
#companies = Company.where(:name => params[:name])
else
#companies = Company.all
end
end
end
I think thats what you mean (tell me if I'm wrong!)
You can't have two methods with the same name in Ruby. If you have multiple methods with the same name, the last method defined will be the one that Ruby uses.